Skip to content

Authoring Invariants

An invariant starts as intent text — a plain-language claim about a system property. Forgecroft translates that intent into a set of evidence checks and surfaces any honest gaps. This page covers the full authoring lifecycle: preview, create, update, re-analyze, and retire.

You need write access at the org level. Preview and create both consume one AI inference; the cost class is returned in the preview result so you can evaluate it before committing.

Preview processes your intent without persisting anything. Use it to inspect the evidence checks, review honest gaps, and check the estimated cost class before creating.

Terminal window
forgecroft invariants preview --intent "no public S3 buckets"

You can also pass intent on stdin:

Terminal window
echo "no public S3 buckets" | forgecroft invariants preview

Or as a positional argument:

Terminal window
forgecroft invariants preview "no public S3 buckets"

The preview output shows the evidence checks, honest gaps, and token usage:

Processed by anthropic (claude-sonnet-4-5)
Estimated cost class: low
Tokens used: 1240 in, 310 out
Evidence checks (1):
1. type=policy engine=opa_rego/v1
Honest gaps (1):
- [RUNTIME_NOT_OBSERVABLE] Bucket ACL configuration applied outside of Terraform
cannot be detected from plan output alone.

Add --json to get the full structured response.

preview, err := client.PreviewInvariant(ctx, "no public S3 buckets")
if err != nil {
return err
}
fmt.Printf("Cost class: %s\n", preview.EstimatedCostClass)
for _, gap := range preview.HonestGaps {
fmt.Printf("Gap [%s]: %s\n", gap.Code, gap.Message)
}
POST /invariants/preview
Content-Type: application/json
{
"intent_text": "no public S3 buckets"
}

Response:

{
"compiled_mix": { ... },
"honest_gaps": [
{
"code": "RUNTIME_NOT_OBSERVABLE",
"message": "Bucket ACL configuration applied outside of Terraform cannot be detected from plan output alone."
}
],
"estimated_cost_class": "low",
"ai_provider": "anthropic",
"ai_model": "claude-sonnet-4-5",
"input_tokens": 1240,
"output_tokens": 310
}
FieldDescription
compiled_mixThe evidence-check plan Forgecroft produced — the evaluation recipe for this invariant
honest_gapsAspects of the intent the available evidence sources cannot fully prove (see below)
estimated_cost_classRelative per-evaluation cost: low, medium, or high
ai_providerThe AI provider that processed the intent
ai_modelThe specific model used
input_tokens / output_tokensToken usage for this preview call

Honest gaps are aspects of your intent that the current evidence sources cannot fully prove. They are surfaced at authoring time so you know upfront what the invariant can and cannot establish.

A gap entry has a code (machine-readable identifier) and a message (human-readable explanation). For example:

  • RUNTIME_NOT_OBSERVABLE — the claim requires runtime state that is not available from plan output.
  • PARTIAL_COVERAGE — the intent covers multiple resource types; evidence is available for some but not all.

Honest gaps are a feature. They mean Forgecroft is precise about what it can prove. An invariant with gaps still evaluates — it may produce Compliant verdicts where evidence exists and Needs evidence verdicts where it does not.

If a gap is unacceptable, you have two options: reword the intent to focus on what is provable, or accept the gap and plan to add the missing evidence source later.

Create processes the intent and persists the invariant in one call. There are no drafts.

Terminal window
forgecroft invariants create --intent "no public S3 buckets"

Via stdin:

Terminal window
echo "no public S3 buckets" | forgecroft invariants create

Output:

Created invariant inv-01j4k... (status=active, source=customer, version=1)
Intent: no public S3 buckets
inv, err := client.CreateInvariant(ctx, "no public S3 buckets")
if err != nil {
return err
}
fmt.Printf("Created %s\n", inv.ID)
POST /invariants
Content-Type: application/json
{
"intent_text": "no public S3 buckets"
}

Returns 201 Created with the full invariant record:

{
"id": "inv-01j4k...",
"org_id": "org-uuid",
"intent_text": "no public S3 buckets",
"intent_version": 1,
"status": "active",
"source": "customer",
"compiled_mix": { ... },
"created_at": "2026-05-11T10:00:00Z",
"last_compiled_at": "2026-05-11T10:00:00Z"
}
Terminal window
forgecroft invariants list
invs, err := client.ListInvariants(ctx)
GET /invariants

Returns all active invariants for the org. Retired invariants are excluded.

Terminal window
forgecroft invariants get <id>
inv, err := client.GetInvariant(ctx, id)
GET /invariants/{id}

PATCH accepts intent_text (triggers re-analysis, bumps intent_version), status, or both.

There is no dedicated patch subcommand. Use the API or SDK for intent updates; use delete to retire.

newIntent := "no public S3 buckets in any region"
inv, err := client.PatchInvariant(ctx, id, sdk.PatchInvariantInput{
IntentText: &newIntent,
})

To change status only:

retired := "retired"
inv, err := client.PatchInvariant(ctx, id, sdk.PatchInvariantInput{
Status: &retired,
})
PATCH /invariants/{id}
Content-Type: application/json
{
"intent_text": "no public S3 buckets in any region"
}

Updating intent_text triggers a fresh analysis and increments intent_version. The compiled_mix is replaced. Updating status alone does not re-analyze.

Accepted status values: active, retired.

Delete is a soft delete — the row is retained for audit, but evaluators skip it on subsequent runs.

Terminal window
forgecroft invariants delete <id>

Output:

Retired invariant inv-01j4k...
err := client.DeleteInvariant(ctx, id)
DELETE /invariants/{id}

Returns 204 No Content. The invariant’s status is set to retired.

Re-analysis re-runs the intent against the current state of evidence sources without changing the intent_text. Use this when you want an existing invariant to take advantage of newly available evidence sources without changing the intent. (The underlying endpoint is named /recompile for historical reasons.)

intent_version is not incremented — only the compiled_mix and last_compiled_at are updated.

Terminal window
forgecroft invariants recompile <id>
inv, err := client.RecompileInvariant(ctx, id)
POST /invariants/{id}/recompile
MethodPathDescription
POST/invariants/previewProcess intent without persisting
POST/invariantsCreate an invariant
GET/invariantsList active invariants
GET/invariants/{id}Get a single invariant
PATCH/invariants/{id}Update intent or status
DELETE/invariants/{id}Retire an invariant (soft delete)
POST/invariants/{id}/recompileRe-analyze against current evidence sources