Project Invariants
Project Invariants capture the core assumptions and explicitly rejected behaviors that shape the design of depends.cc. These principles ensure simplicity, predictability, and a clear separation of concerns between structure (defined declaratively), runtime state (managed imperatively), and notifications (triggered reactively). Each invariant is enforced in the codebase, often with explicit checks or architectural decisions. Violating them risks breaking user expectations, compatibility, or performance.
The server remains a passive receiver: services push state via API; depends.cc computes effective states and dispatches notifications. No proactive outbound activity occurs beyond webhook delivery.
Key Invariants
No Polling or Background Fetching
The server never polls external services, runs health checks, or performs outbound requests except for webhook and email notifications. Health checks reside in the CLI (depends check), intended for user cron jobs.
Why? This keeps the server lightweight, with zero external dependencies and no scheduler. Users control polling cadence.
Enforcement: No timers, cron, or fetchers exist in server code. meta.checks in depends.yml are ignored by the server; the CLI parses and executes them.
See also: CLI commands, Graph computation
State Exclusion from Configuration Files
Node states (green/yellow/red) never appear in depends.yml or graph YAML imports. YAML defines static structure (nodes, edges, notifications); states are runtime-only, set via API or CLI.
Why? Separates declarative structure from ephemeral state. Importing YAML preserves existing states, avoiding accidental resets.
Enforcement: src/graph/yaml.ts skips state fields. API endpoints like PUT /state/{ns}/{id}/{state} handle state mutations separately.
Example YAML import ignores state:
nodes:
api-server:
state: green # Ignored; structure only
depends_on: [database]
See also: Core model, Configuration
TTL Not Imported from YAML
Per-node TTL values (e.g., "10m") cannot be set via YAML imports, even though they are valid node metadata.
Why? TTL is runtime configuration, akin to state. YAML focuses on graph topology.
Enforcement: src/graph/yaml.ts omits ttl. Set via PUT /nodes/{ns}/{id} with {"ttl": "10m"}.
See also: Graph computation
TTL Escalation Limited to Green → Yellow
TTL acts as a liveness probe: expired green nodes resolve to yellow. yellow or red nodes ignore TTL; they remain unchanged until explicitly updated.
Why? Distinguishes "stale heartbeat" (yellow) from explicit failures (red). Prevents TTL from masking persistent issues.
Enforcement: In src/graph/effective.ts:
function resolveNodeState(node: { state: string; ttl: number | null; last_state_write: string | null }): string {
if (!node.ttl || !node.last_state_write) return node.state;
if (node.state !== "green") return node.state; // Non-green ignores TTL
// ... expiry check flips green → yellow only
}
See also: Core model, Database schema
Lazy TTL Evaluation, No Background Sweeper
TTL expiry computes on-demand during reads (e.g., effective state calculation). No periodic jobs scan for expirations.
Why? Avoids cron overhead; evaluation triggers naturally on state writes or queries. Ensures low idle CPU.
Enforcement: Lazy resolution in computeEffectiveState. Webhooks fire only on triggered evaluations.
Pitfall: Silent expirations until interaction; mitigate with cron-run depends check.
Account-Scoped Tokens, Not Namespace-Scoped
Tokens (dep_...) grant access to all namespaces owned by an account. No per-namespace tokens or user rotation.
Why? Simplifies auth: one token per user. Admins manage via depends admin tokens.
Enforcement: src/auth.ts verifies token ownership without namespace scoping beyond existence checks.
See also: Authentication, Hosted mode, Self-hosted mode
Cycles Rejected at Structure Import
Dependency cycles are detected and rejected during YAML import or edge insertion. No runtime cycle handling needed.
Why? Guarantees terminating traversals for effective state. Simplifies graph code.
Enforcement: src/graph/cycle.ts BFS detects paths back to source. src/graph/yaml.ts rejects on import.
See also: Graph computation
Self-Hosted Mode Bypasses Bearer Auth
Any Authorization: Bearer ... (or none) maps to LOCAL_TOKEN (dep_local). No validation occurs.
Why? Frictionless local dev: curl http://localhost:3000/v1/... works without tokens.
Enforcement: src/auth.ts and src/server/middleware.ts short-circuit for self-hosted.
See also: Self-hosted mode
Ack URLs Only Unsuppress Rules
Visiting an ack_url clears a notification rule's suppressed flag. Node states remain untouched.
Why? Ack manages alert fatigue, not state. Keeps concerns separate.
Enforcement: Dedicated /v1/ack/{token} route updates only notification_rules.suppressed.
See also: Notifications
Webhook Signatures Use Raw Body Bytes
X-Signature HMACs the exact request body bytes, not re-serialized JSON.
Why? Canonical verification; avoids serialization ambiguities (key order, whitespace).
Enforcement: src/notify/webhook.ts computes HMAC on rawBody.
Verification example:
import hmac, hashlib
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers["X-Signature"])
See also: Notifications
Design Rationale
These invariants prioritize a minimal server footprint, explicit user control, and DAG semantics. They reject common pitfalls like state creep into config or hidden background processes, ensuring depends.cc remains a reliable "state mirror" for dependency graphs.
Repo references: Invariants align with files like src/graph/effective.ts (TTL/effective state), src/notify/dispatcher.ts (notifications), and src/auth.ts (tokens/modes).
Recent changes
- Created: New project-invariants.md docs core invariants like no polling and passive server