Invariants
Enforced standing constraints. What must stay true while work flows. Vocabulary discipline: see VOCABULARY.md. Reference: METHOD.md for full methodology.
Definition
An Invariant is an enforced standing constraint that applies to all work within its scope. HIPAA encryption on every PHI write. Brand voice on every published post. A statute citation on every civic recommendation. A passing browser smoke before every production deploy. These are not deliverables — they are conditions that must hold while the system runs.
An Invariant is not an Outcome. Outcomes describe change ("Customers can book online"). Invariants describe permanence ("Patient data is encrypted at rest"). When you ship an Outcome, the Outcome completes and moves to Done. An Invariant never completes; it has a health_status that is either passing, failing, or unknown, and it surfaces violations as Attention Alerts in the Signal layer.
An Invariant is not a Context.constraint. Context.constraint is passive documentation — it records that a limit exists. An Invariant has an enforcement mechanism: compile-time (lint), deploy-time (gate), AI-verified (review pass), or manual (human attestation). Without enforcement, a constraint is a hope. With enforcement, it is an Invariant.
Invariants do not move through Flow stages. They are scoped to a workspace, a project, or a clan. Outcomes within that scope auto-link by tag (e.g., an Invariant with auto_link_tags: ["phi"] links to every Outcome tagged phi). When an Outcome's Build or Review surfaces a violation, the Invariant's health_status flips to failing, the violation is recorded, and the Signal layer raises an Attention Alert. When the violation is resolved, the status flips back.
The promotion from Context.constraint to first-class Invariant is the v7.0 audit's single largest methodology change. Eight of twenty-five audited clans were carrying standing rules — HIPAA, SOC 2, brand voice, audit-trail, no-hardcoded-hex, pre-deploy gates — that needed enforcement, not just documentation. The pre-v7.0 method gave them only Context to record the rule and Outcomes to do the work. Reality showed the same constraint reappearing as new Outcomes every time a feature touched the regulated surface, with reviewers losing the thread by the tenth occurrence. The Invariant primitive collapses that pattern into one declaration, one enforcement mechanism, and one health signal.
Invariants are checked, not assumed. An Invariant with enforcement: manual is still an Invariant — it has an owner, a last_checked_at timestamp, and a violation_count_30d count. Manual checks decay; the rolling count makes the decay visible. If you never look at the count, you have a documented rule with no enforcement, which is the anti-pattern the primitive exists to prevent.
Field Table
| Field | Type | Required | Default | Description | Notes |
|---|---|---|---|---|---|
| id | uuid | yes | — | Unique identifier | — |
| name | string | yes | — | Human-readable name (e.g., "HIPAA Patient Data Encryption") | Max 200 chars |
| description | text | yes | — | What the invariant guarantees | — |
| scope | enum | yes | — | Where it applies | workspace / project / clan_wide |
| enforcement | enum | yes | — | How it's checked | compile / deploy / ai_verified / manual |
| auto_link_tags | string[] | no | [] |
Outcome tags that auto-link to this invariant | e.g., ["security", "hipaa"] |
| health_status | enum | no | unknown | Current state | passing / failing / unknown — computed, not user-set |
| last_checked_at | timestamp | no | null | When health was last evaluated | Computed |
| violation_count_30d | int | no | 0 | Rolling violation count for risk heat-map | Computed |
| created_at | timestamp | yes | now() | — | — |
| created_by | uuid | yes | — | User who declared the invariant | — |
enforcement enum
| Value | Meaning |
|---|---|
| compile | Enforced at build / lint time. Hardcoded hex catches at PR. Type mismatches catch in tsc. |
| deploy | Enforced at the deploy gate. Browser-smoke must pass before release proceeds. |
| ai_verified | A reviewing agent (Claude Code or equivalent) checks the Invariant against changed code or content. |
| manual | A human signs off. Used when no automated check is feasible and the cost of a missed violation justifies the friction. |
scope enum
| Value | Meaning |
|---|---|
| workspace | Applies to every project in the workspace. Compliance regimes (HIPAA, SOC 2) usually live here. |
| project | Applies to one project only. Per-app brand voice, per-product launch gates. |
| clan_wide | Applies across all projects owned by a clan. Cross-product engineering standards. |
health_status enum
| Value | Meaning |
|---|---|
| passing | Last check passed. Invariant holds. |
| failing | Last check found a violation. Attention Alert is active until resolved. |
| unknown | Never checked, or check failed to run. Treated as a soft failure — investigated, not ignored. |
This table is the source of truth. Phase 13 derives the Zod schema in @canopy/shared directly from this table. Any field name, type, or constraint change must update this table first.
Snake_case canonical for enum values. Postgres enum values cannot contain hyphens, so DB / API / CLI use
ai_verifiedandclan_wide. Narrative prose may write "AI-verified" or "clan-wide" outside structured contexts (table cells, code examples, JSON), but every machine-consumed surface (DB enums, Zod validators, tRPC inputs, web form values, CLI args, methodology field tables) uses the snake_case form. The cross-surface CI gate (scripts/check-cross-surface-fields.cjs) enforces this.
Examples
Deep example: MOIMF — Pre-deploy browser-smoke gate
The MOIMF clan ships through a deploy pipeline. Production releases must pass a browser-smoke harness — a synthetic user journey through the live build — before traffic is cut over. The rule predated the Invariant primitive; it lived as prose in MOIMF-LENS-001 and was enforced ad-hoc, sometimes skipped under deadline pressure.
The Invariant record:
| Field | Value |
|---|---|
| name | Pre-deploy browser smoke must pass |
| description | Every production deploy runs the browser-smoke harness against the candidate build. All checks must pass before traffic cuts over. Failures block deploy. |
| scope | project |
| enforcement | deploy |
| auto_link_tags | ["deploy", "production"] |
| health_status | passing |
Why isn't this an Outcome? Building the smoke harness IS an Outcome — finite work, success criteria, ships. The standing rule "smoke must pass before deploy" is the Invariant. Each deploy is a check against the Invariant, not an instance of it. When a deploy fails the smoke check, you don't reopen the harness Outcome; you investigate the violation. The Invariant tracks the gate's health across every deploy attempt.
Audit reference: MOIMF-LENS-001 in MOIMF's .clan-alpha/ tree; v7.0 decision rationale in DECISIONS-RESOLVED.md#D-02.
Short examples
DCD — HIPAA patient-data encryption. Healthcare clan. Every record write that touches PHI must encrypt at rest. Scope: clan_wide. Enforcement: ai_verified (Claude Code reviews diffs for PHI columns and confirms encryption).
auto_link_tags: ["phi", "patient"]. Replaces the old pattern of repeating HIPAA "operating principles" in every Outcome's success criteria. Before the Invariant: 12 Outcomes touching PHI across two years, three caught the encryption requirement, nine relied on the developer remembering. After: one Invariant, every PHI-tagged Outcome auto-links, the AI reviewer catches misses at PR.Scribe — No hardcoded hex values. Content / brand clan. All colors must come from the design-token system. Scope: project. Enforcement: compile (lint rule that fails on
#[0-9A-F]{6}in.tsx).auto_link_tags: ["styles", "ui"]. Replaces the prose "Never hardcode a hex value" in CLAUDE.md that was ignored under deadline. The lint rule is the Invariant's enforcement leg; the rule's existence is the Invariant itself.Trellis — Statute citation on every civic recommendation. Civic / township platform. Every recommendation produced by the system must cite the Michigan statute that grants the authority. Scope: project. Enforcement: ai_verified (reviewer checks every published recommendation for a statute reference).
auto_link_tags: ["recommendation", "civic"]. Replaces the unwritten convention that produced inconsistent citations across published guidance. The Invariant gave the rule a name, an owner, and a place to track misses.Barrel — Audit trail on every record mutation. Audit-trail clan. Every mutation to a tracked record must write an
audit_logrow withactor,before,after. Scope: clan_wide. Enforcement: ai_verified (review pass) plus a compile-time helper that fails the build if the audit-log call is missing from a known mutation path.auto_link_tags: ["mutation"]. Replaces the buried-in-success-criteria pattern that reviewers stopped noticing.
Anti-patterns
Anti-pattern: Tracking standing rules as repeating Outcomes
Eight clans in the v7.0 audit caught this. DCD's HIPAA operating principles reappeared as Outcomes every time a feature touched patient data. TestKillerProject tracked SOC 2 readiness as ~80 distinct Outcomes instead of one Invariant with violation events. Each Outcome shipped, then the next one for the next feature, then the next, indefinitely. The work was real but it kept looking like new work.
The correct framing: one Invariant, scope clan_wide or project-wide, auto_link_tags matching the relevant Outcome tags. Outcomes that touch PHI link to the Invariant automatically. Violations surface as Attention Alerts. The Outcome lifecycle goes back to describing change, where it belongs.
Anti-pattern: Writing the constraint as Context only and hoping people read it
Scribe's CLAUDE.md says "Never hardcode a hex value." That is Context — passive. Code with hardcoded hex still shipped, because nobody re-reads CLAUDE.md while writing a component.
The correct framing: same rule, restated as an Invariant. Scope: project. Enforcement: compile. Now the lint rule catches the violation at PR time and the health_status makes the failure visible. Documentation moves from CLAUDE.md prose into the enforced spec.
Anti-pattern: Declaring an Invariant with no enforcement mechanism
A clan declares Invariant: All API endpoints must rate-limit at 100 req/min/user. Scope project, enforcement... unset. Six months later, three endpoints have no rate limit. Nobody checked because nothing was responsible for checking. The Invariant lived in the system but did no work.
The correct framing: choose the enforcement leg at declaration time. Compile (middleware-required lint rule). Deploy (synthetic load test before release). AI-verified (review-pass over changed endpoints). Manual (named owner attests monthly, decay surfaced via violation_count_30d). An Invariant without enforcement is a Context.constraint with the wrong filing label.
Anti-pattern: Repeating the same constraint in every Outcome's success criteria
Barrel's audit-trail requirement appeared in every Outcome's "Definition of Done" — "audit_log entry written for any record mutation." Reviewers stopped reading the bullet around the tenth occurrence; missed violations shipped.
The correct framing: one Invariant, auto_link_tags: ["mutation"]. The check runs once per affected Outcome at Review time. Violations surface as Attention Alerts instead of buried in a checklist. Reviewers spend their attention on the parts of the work that are actually different.
Cross-refs
- METHOD.md — Invariant joins the 9 method primitives (was 5 in v1.0)
- OUTCOMES.md — Outcome vs Invariant disambiguation (Outcomes = change, Invariants = permanence)
- CONTEXT.md — Context.constraint vs Invariant disambiguation (passive doc vs enforced gate)
- FLOW-AND-SIGNAL.md — Invariant violations surface as Attention Alerts in the Signal layer
- VOCABULARY.md — preferred terms
- PORTFOLIO.md — clan context for MOIMF, DCD, Scribe, Trellis
docs/methodology/validation/VALIDATION.md— 8-clan audit evidence that motivated promotiondocs/methodology/validation/DECISIONS-RESOLVED.md— D-02 origin decision
Relationship to Phase 13
This doc is the contract Phase 13 (Invariant Primitive — schema + UI + health dashboard) must satisfy. The Field Table above is the source of truth for:
- The Zod schema in
@canopy/shared/invariant.ts(one row → one schema field) - The Drizzle table definition in
@canopy/db/schema/invariant.ts - The tRPC procedures in
@canopy/api/router/invariant.ts(CRUD + health-status mutations) - The Invariant detail / list / health-dashboard UI in
apps/web/app/[workspace]/invariants/
If the methodology field set needs to change after Phase 13 ships (e.g., a new enforcement mode is needed for a clan onboarded later), the change starts here: edit this table first, then propagate. The reverse direction — changing the schema and back-filling this doc — is the anti-pattern that creates docs↔code drift.