Three ways in

Rules you can mint

A factory's rules are generic constraints from one small vocabulary — there is no Effect::CastVote or Effect::ClaimBounty anywhere in the system. A ballot that cannot be voted twice is a WriteOnce slot; a budget that cannot exceed its ceiling is a FieldLteField pair; a workflow that cannot rewind is a StrictMonotonic counter. The full constraint grammar, generated from the canonical source:

constraintfieldssemantics
FieldEquals simpleindex: u8, value: FieldField at index must equal value.
FieldGte simpleindex: u8, value: FieldField at index must be >= value (unsigned big-endian comparison).
FieldLte simpleindex: u8, value: FieldField at index must be <= value (unsigned big-endian comparison).
FieldLteFieldleft_index: u8, right_index: u8Field at left_index must be <= field at right_index in the same post-state.
FieldLteOtherindex: u8, other: u8, delta: i64Field at index must be <= field at other + delta in the same post-state (new[index] <= new[other] + delta, signed).
SumEqualsindices: Vec<u8>, value: FieldSum of fields at indices must equal value (intra-cell conservation).
WriteOnce simpleindex: u8Slot must transition only from FIELD_ZERO to any non-zero value; after the first write, the slot is frozen.
Immutable simpleindex: u8Slot value is read-only after initialization.
Monotonic simpleindex: u8new[i] >= old[i] (unsigned big-endian).
StrictMonotonic simpleindex: u8new[i] > old[i] strictly.
BoundedBy simpleindex: u8, witness_index: u8slot[index] may only be set (i.e.
FieldDeltaindex: u8, delta: Fieldnew[index] == old[index] + delta (modular field arithmetic).
FieldDeltaInRangeindex: u8, min_delta: Field, max_delta: Fieldnew[index] in [old[index] + min_delta, old[index] + max_delta].
FieldGteHeight simpleindex: u8, offset: i64new[index] >= ctx.block_height + offset.
FieldLteHeight simpleindex: u8, offset: i64new[index] <= ctx.block_height + offset.
SumEqualsAcrossinput_fields: Vec<u8>, output_fields: Vec<u8>Intra-cell conservation across the transition: sum(new[input_fields]) == sum(old[input_fields]) + sum(new[output_fields]).
SenderAuthorizedset: AuthorizedSetThe turn's sender must be in an authorized set.
CapabilityUniquenesscap_set_root_slot: u8slot[cap_set_root_slot] is a per-cell capability-set root and must encode at most one live capability of the named kind.
RateLimitmax_per_epoch: u32, epoch_duration: u64Sender may mutate this cell at most max_per_epoch times per epoch_duration blocks.
RateLimitBySumslot_index: u8, max_sum_per_epoch: u64, epoch_duration: u64Sum-based rate limit: the *value* added to slot_index over a window of epoch_duration blocks cannot exceed max_sum_per_epoch.
TemporalGate simplenot_before: Option<u64>, not_after: Option<u64>Mutation is rejected unless ctx.block_height is in [not_before, not_after].
PreimageGate simplecommitment_index: u8, hash_kind: HashKindThe action must reveal a preimage whose hash equals slot[commitment_index].
MonotonicSequenceseq_index: u8slot[seq_index] == old[seq_index] + 1.
AllowedTransitionsslot_index: u8, allowed: Vec<(Field, Field)>(old[slot_index], new[slot_index]) must appear in the explicit allow-list allowed.
TemporalPredicatewitness_index: u8, dsl_hash: Hash32Witness-attached temporal-predicate proof.
BoundDeltalocal_slot: u8, peer_cell: CellId, peer_slot: u8, delta_relation: DeltaRelationCross-cell binding pair to γ.2: this cell's local_slot delta must match peer_cell's peer_slot delta under the named DeltaRelation.
AnyOfvariants: Vec<SimpleStateConstraint>Single-level disjunction: at least one of variants must hold.
Witnessedwp: WitnessedPredicateA witness-attached predicate (DFA classification, temporal-DSL proof, blinded-set non-revocation, bridge predicate, custom AIR…).
Renouncedset: RenouncedSet**Categorical dual of SenderAuthorized: proof of non-holding / non-membership.** A *renunciation* slot caveat — the action's sender must verifiably *NOT* be in the set's sorted Merkle leaf set.
MemberOfindex: u8, set: Vec<u64>**Value allowlist:** new[index] must be one of set (the one-sided value membership the pair-table AllowedTransitions cannot express).
PrefixOfseg_indices: Vec<u8>, prefix: Vec<u64>**Namespace / path prefix containment:** the ordered scalar path read from seg_indices (each a slot read as big-endian u64) must START WITH prefix.
InRangeTwoSidedindex: u8, lo: u64, hi: u64**Two-sided absolute band:** lo <= new[index] <= hi (the ABSOLUTE counterpart to the RELATIVE FieldDeltaInRange).
DeltaBoundedindex: u8, d: u64**Real two-sided delta:** |new[index] - old[index]| <= d (symmetric; the legacy delta variants are one-sided or relative-range).
AffineLeterms: Vec<(i64, u8)>, c: i64**Affine inequality:** Σ kᵢ·new[fᵢ] <= c over named slots (terms : Vec<(i64 coefficient, u8 slot)>).
AffineEqterms: Vec<(i64, u8)>, c: i64**Affine equation:** Σ kᵢ·new[fᵢ] = c.
Reachablefrom_index: u8, to_label: u64, edges: Vec<(u64, u64)>**DAG reachability / prerequisite:** the label read from new[from_index] must reach to_label in the reachability edges (reflexive-transitive closure).
AllOfvariants: Vec<SimpleStateConstraint>**n-ary conjunction** over SimpleStateConstraints — the allOf the legacy 2-level grammar lacked (it had only single-level AnyOf).
Customir_hash: Hash32, descriptor: CustomDescriptor, reads: ReadSetDSL-authored predicate.
SenderIs simplepk: Hash32The turn's sender (acting cell's public key) must equal pk.
SenderInSlot simpleindex: u8The turn's sender must equal the identity held in new[index].
BalanceGte simplemin: u64The cell's own post-turn balance must be >= min.
BalanceLte simplemax: u64The cell's own post-turn balance must be <= max.
KeyRotationGatedigest_slot: u8, current_slot: u8, last_rotated_slot: u8, cooling_period: u64, hash_kind: HashKind**Pre-rotation gate (KERI-shaped)** — the identity rider (docs/ORGANS.md "Identity rider"; kernel semantics proven in metatheory/Dregg2/Apps/PreRotation.lean, the rotateWriteCooled production shape).
HeapField simplekey: u64, atom: HeapAtom**Heap-keyed atom (THE ROTATION's app-state lane)** — the top-level lift of HeapAtom over heap key key, the twin of HeapField (both surfaces share ONE evaluator arm, like the SenderIs/BalanceGte precedent).
DelegationEpochEquals simpleindex: u8new[index] must equal the touched cell's post-turn delegation_epoch.
CountGe simplethreshold: u32, set_commitment_slot: u8The witness must exhibit ≥ threshold distinct elements opening the commitment in new[set_commitment_slot].
SenderMemberOf simplemembers: Vec<Hash32>The turn's sender must be one of members.
BalanceDeltaLte simplemax: i64The cell's per-turn balance change is <= max.
BalanceDeltaGte simplemin: i64The cell's per-turn balance change is >= min.
AffineDeltaLeterms: Vec<(i64, u8)>, c: i64**Multi-field delta gate:** Σ kᵢ·(new[fᵢ] − old[fᵢ]) <= c over named slots (terms : Vec<(i64 coefficient, u8 slot)>).
ObservedFieldEqualslocal_field: u8, source_cell: Hash32, source_field: u8, at_root: Hash32, proof_witness_index: usize**Cross-cell verified observation** (docs/CELL-PROGRAM-LANGUAGE.md §11.2): new[local_field] must equal the value source_field held by the PEER cell source_cell at the FINALIZED state-commitment root at_root.
CollectionAggregatecollection_id: u32, stride: u32, fuel: u32, pred: CollPred**Aggregate-over-a-collection gate** (docs/CELL-PROGRAM-LANGUAGE.md gaps 7/11.1 — the heap/layout rung, the documented "lamesauce" N≤3 fixed-slot cap *lifted*).
AnyOfBoundbranches: Vec<BoundBranch>**Witnessed branches under disjunction** (docs/CELL-PROGRAM-LANGUAGE.md §11.3 — the AnyOfBound rung) — admits IFF SOME branches element admits.
SymEqindex: u8, sym: u64**SymEq** — field_to_u64(new[index]) == sym: the field's interned identity (the u64 lane) equals sym.
SymMemberOfindex: u8, set: Vec<u64>**SymMemberOf** — field_to_u64(new[index]) ∈ set: enum membership by the symbol lane ("status ∈ {Draft, Active, Frozen}").
DigEqindex: u8, digest: Field**DigEq** — new[index] == digest as a FULL 32-byte field: the field's digest / cell-reference equals digest.
DigFieldEqleft_index: u8, right_index: u8**DigFieldEq** — new[left_index] == new[right_index] as FULL 32-byte fields: two digest slots are equal.
57 constraint kinds, generated from cell/src/program.rs (the doc-commented canonical enum), cross-checked against the JSON projection the studio renders.

What factories dissolve into

Recurring shapes — escrow, obligation, council, constitution — are factory patterns with in-tree proof modules. These worked descriptors are generated by running the real constructors; open one in the Studio composer and edit it:

Escrow (per-deal settlement cell)

descriptor hash 9346eda1daff7e3a… · cell/src/blueprint.rs escrow_factory_descriptor — Lean twin Dregg2.Apps.EscrowFactory

Obligation (bonded-proof settlement cell)

descriptor hash f344686ad154a437… · cell/src/blueprint.rs obligation_factory_descriptor — Lean twin Dregg2.Apps.ObligationFactory

Council proposal (M-of-N governance cell)

descriptor hash 6582dc71f941bf34… · starbridge-apps/polis council_factory_descriptor — DRAFT→PROPOSED→{REJECTED, APPROVED→EXECUTED}, 2-of-3

Constitution (per-version parameter cell)

descriptor hash e418326b41be6968… · starbridge-apps/polis constitution_factory_descriptor — UNINIT→ACTIVE→SUPERSEDED, params pinned for life

4 worked factory descriptors, generated by running the real Rust constructors (site/tools/gen-factory-samples.sh). Open one in the Studio composer to edit it, or inspect the machine it builds (the composer mounts the matching polis inspector on recognizable machines).

Eight real apps, no enforcement code

Each is a real Rust crate with factory descriptors, signed turn-builders, and a passing test suite, running end-to-end against the canonical executor. The live devnet seeds one of each at genesis. To walk any of them — slots, constraints, turns, and a live try-it against a node — open the worked-examples walker.

One image, four surfaces

While you build, everything is inspectable as a dregg:// URI rendered by the same platform inspectors everywhere: the explorer for live objects, the playground for driving the wasm executor, the Studio for authoring, and Starbridge — the shell you boot into your polis: one frame holding your identity, your cells, the live receipt stream, and the app places (its inspector IDE lives at workbench).

Drive an app from the CLI

A rejected second claim or a tally that refuses to shrink is the constraint biting on the verified commit path — not a string compare in the app:

# Privacy voting, live against a node
dregg voting open  --question "ship the gallery port?"
dregg voting tally --poll <id> --choice yes
dregg voting close --poll <id>

# Bounty board lifecycle (first-claimer-wins)
dregg bounty post   --title "port gallery" --reward 500
dregg bounty claim  --bounty <id>
dregg bounty submit --bounty <id> --uri ipfs://...
dregg bounty payout --bounty <id>

Each app's source and tests live under starbridge-apps/<name>/ in the repo. The deeper grounding — how apps inherit theorems by consuming receipts against descriptors — is rung 6 of the protocol docs.