Skip to content

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.

CheckSeverity in .run()Severity in .guard()What it catches
DuplicateerrorerrorA Task or Binding registered more than once across all stages
UnsatisfiederrorerrorA Task or Binding needs context not provided by any preceding stage
UnusedwarningerrorA Binding is never consumed by any Task or Binding

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 starts

Duplicate 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() 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()
})
})