Summary
loop() resolves with success: true when no step actually ran — either because config.steps is empty, or because every step was skipped via its when() predicate. "Nothing executed" is reported as a successful loop.
Where
packages/core/src/loop.ts
The terminal result derives success from Array.prototype.every:
// packages/core/src/loop.ts (final return)
return {
success: results.every((r) => r.success), // [].every(...) === true
results,
duration: Date.now() - start,
completedSteps: results.filter((r) => r.success).length, // 0
totalSteps: config.steps.length,
};
results only receives entries for steps that actually executed. A step whose when() returns false hits continue (if (step.when && !step.when(results)) continue;) and never pushes to results. So when zero steps run, results is [], [].every(...) is vacuously true, and the loop returns:
{ success: true, results: [], completedSteps: 0, totalSteps: N }
Reproduction
import { loop } from "@codespar/sdk";
// Case 1 — empty step list
const r1 = await loop(session, { steps: [] });
// r1.success === true, r1.completedSteps === 0, r1.totalSteps === 0
// Case 2 — every step gated off by when()
const r2 = await loop(session, {
steps: [
{ tool: "asaas/create_payment", params: {}, when: () => false },
{ tool: "nuvemfiscal/issue_invoice", params: {}, when: () => false },
],
});
// r2.success === true, r2.completedSteps === 0, r2.totalSteps === 2
A caller that branches on result.success cannot distinguish "the whole commerce flow completed" from "nothing happened at all". For a library that orchestrates real money movement (charge → invoice → shipping → notification), a vacuously-successful loop is an easy way to silently no-op a payment flow and still log success.
Related shape in this codebase
This is the same vacuous-truth pattern as the waitForConnections guard, which was hardened to conns.length > 0 && conns.every(...) (PR #36). loop() has the analogous gap and was deliberately left as-is there pending this decision.
Cross-SDK note
The Python client (codespar) does not ship a loop() / Complete Loop API at all — it is not in packages/python/src/codespar/__init__.py __all__. So there is no current TS↔Python parity constraint here; this is purely a decision about the TypeScript public contract. If Complete Loop is ever ported to Python, whatever is decided here should be mirrored there.
Decision needed (public contract change)
Changing loop()'s success semantics is a behavior change to a documented public API, so it needs a maintainer call:
- (a) Treat "nothing ran" as non-success. When
results.length === 0 (and totalSteps > 0, or even for steps: []), return success: false — or surface a distinct outcome (e.g. skipped: true / completedSteps === 0 && totalSteps > 0) so callers can tell apart "all good" from "all skipped".
- (b) Document as intentional. A loop with nothing to do trivially succeeds; document explicitly that
success: true, completedSteps: 0 means "no step was applicable" and put the burden on callers to check completedSteps / totalSteps.
Option (a) is the safer default for a money-movement orchestration primitive; (b) is acceptable if it is explicitly documented on LoopResult. Filing for the maintainer's decision before any code change — happy to send the PR (with tests for both empty-steps and all-skipped) once the direction is chosen.
Suggested acceptance criteria
- Behavior for
steps: [] and "all when() false" is explicitly specified (in code + LoopResult doc comment in @codespar/types).
- Whichever option is chosen has regression tests covering both the empty-steps case and the all-skipped case.
Summary
loop()resolves withsuccess: truewhen no step actually ran — either becauseconfig.stepsis empty, or because every step was skipped via itswhen()predicate. "Nothing executed" is reported as a successful loop.Where
packages/core/src/loop.tsThe terminal result derives success from
Array.prototype.every:resultsonly receives entries for steps that actually executed. A step whosewhen()returns false hitscontinue(if (step.when && !step.when(results)) continue;) and never pushes toresults. So when zero steps run,resultsis[],[].every(...)is vacuouslytrue, and the loop returns:Reproduction
A caller that branches on
result.successcannot distinguish "the whole commerce flow completed" from "nothing happened at all". For a library that orchestrates real money movement (charge → invoice → shipping → notification), a vacuously-successful loop is an easy way to silently no-op a payment flow and still log success.Related shape in this codebase
This is the same vacuous-truth pattern as the
waitForConnectionsguard, which was hardened toconns.length > 0 && conns.every(...)(PR #36).loop()has the analogous gap and was deliberately left as-is there pending this decision.Cross-SDK note
The Python client (
codespar) does not ship aloop()/ Complete Loop API at all — it is not inpackages/python/src/codespar/__init__.py__all__. So there is no current TS↔Python parity constraint here; this is purely a decision about the TypeScript public contract. If Complete Loop is ever ported to Python, whatever is decided here should be mirrored there.Decision needed (public contract change)
Changing
loop()'s success semantics is a behavior change to a documented public API, so it needs a maintainer call:results.length === 0(andtotalSteps > 0, or even forsteps: []), returnsuccess: false— or surface a distinct outcome (e.g.skipped: true/completedSteps === 0 && totalSteps > 0) so callers can tell apart "all good" from "all skipped".success: true, completedSteps: 0means "no step was applicable" and put the burden on callers to checkcompletedSteps/totalSteps.Option (a) is the safer default for a money-movement orchestration primitive; (b) is acceptable if it is explicitly documented on
LoopResult. Filing for the maintainer's decision before any code change — happy to send the PR (with tests for both empty-steps and all-skipped) once the direction is chosen.Suggested acceptance criteria
steps: []and "allwhen()false" is explicitly specified (in code +LoopResultdoc comment in@codespar/types).