Checking Configuration
Guard statically validates your composition before the app starts. It catches structural problems — like a task consuming context that was never provided — so they never reach production.
Guard performs three checks. When an error is found, it stops immediately — fix it, then run again to catch the next one.
| Check | Severity in .run() | Severity in .guard() | What it catches |
|---|---|---|---|
| Duplicate | error | error | A Task or Binding registered more than once across all stages |
| Unsatisfied | error | error | A Task or Binding needs context not provided by any preceding stage |
| Unused | warning | error | A Binding is never consumed by any Task or Binding |
Built into run
Section titled “Built into run”Guard runs automatically every time you call .run(). If an error is found, .run() rejects before any task starts — the app won’t start with a broken configuration.
compose() .stage({ steps: [bind(apiUrl, literal("https://api.example.com"))] }) .stage({ steps: [auth, dashboard] }) .run() // guard runs here, statically, before the app startsDuplicate and unsatisfied issues reject with an error. Unused bindings print a console.warn — they don’t block the app but signal dead configuration worth cleaning up.
Guard in CI
Section titled “Guard in CI”.guard() is the standalone version. It performs the same three checks but is stricter: unused bindings also throw, not just warn.
Call it in a test to assert that your composition is correctly wired:
import { describe, expect, it } from "vitest"import { bind, compose, literal } from "@grlt-hub/app-compose"import { apiUrl, auth, dashboard } from "./features"
const stages = compose() .stage({ steps: [bind(apiUrl, literal("https://api.example.com"))] }) .stage({ steps: [auth] }) .stage({ steps: [dashboard] })
describe("app configuration", () => { it("is valid", () => { expect(() => stages.guard()).not.toThrow() })})This makes broken wiring a CI failure. If a team member removes a task or renames a tag without updating the composition, the test catches it before the change ships.
Try commenting out the first stage or uncommenting the duplicate to see guard catch it:
const apiUrl = createTag({ name: "apiUrl" })
const auth = createTask({ name: "auth", run: { context: { url: apiUrl.value }, fn: ({ url }) => ({ token: "secret", url }), },})
const dashboard = createTask({ name: "dashboard", run: { context: { token: auth.result.token }, fn: ({ token }) => console.log(`dashboard: token=${token}`), },})
const stages = compose() // 👇 try commenting this out .stage({ steps: [bind(apiUrl, literal("https://api.example.com"))] }) .stage({ steps: [auth] }) // 👇 or uncomment this to add a duplicate // .stage({ steps: [auth] }) .stage({ steps: [dashboard] })
describe("app configuration", () => { it("is valid", () => { expect(() => stages.guard()).not.toThrow() })})