# Writing constraints

> A constraint states a boundary in a form the model can quote back and a reviewer can check against a line.

Page: PAG · Guide
Canonical: https://banes-lab.com/pag/guide#writing-constraints

This section is stop 19 of 102 in the learning route. Previous: [04 - Node design](https://banes-lab.com/pag/guide/node-design.md). Next: [06 - Well-formedness](https://banes-lab.com/pag/guide/well-formedness.md). It builds on [04 - Node design](https://banes-lab.com/pag/guide/node-design.md).

A constraint states a boundary in a form the model can quote back and a reviewer can check against a line. It has four parts: a property that could be false, the set it ranges over, the parties it binds, and the objector that would disagree if it stopped holding; [E1·e four slots](https://banes-lab.com/pag/guide#writing-constraints-panel-e) shows these parts, and [E1·a invariant records](https://banes-lab.com/pag/guide#writing-constraints-panel-a) fills them in. A rule that holds everywhere names every node as its set, and a rule that holds inside a context names that context. The invariant block is the constrain stage of [the loop](https://banes-lab.com/disciplined-methodology/start/the-loop.md). It decides what is admissible and yields a boolean over the work rather than an opinion about it, and it closes the document, as shown in [E1·c worked document](https://banes-lab.com/pag/guide#writing-constraints-panel-c). A rule written as encouragement is judged rather than checked, as traced in [E1·d exhortation or record](https://banes-lab.com/pag/guide#writing-constraints-panel-d). A rule written as a bullet under a heading carries no set and no objector, so nothing can say when it was broken, and [E1·b exhortation rewritten](https://banes-lab.com/pag/guide#writing-constraints-panel-b) shows the repair.

### Rules the model can quote

A rule stated as an exhortation binds nothing, because neither you nor a check can say when it was broken. A document says handle errors properly, the model wraps some operations and not others, and the reviewer cannot say the rule was broken because the rule never said what handling was. A model completes an exhortation with whatever careful looks like in its training, and a specific prohibition with the thing it names; a record with a named objector is the only form in which a reviewer and a check read the same rule.

For this reason a behavioural boundary is written as a checkable record with its objector, not as guidance. The reach of a broad exhortation is traded for the checkability of a narrow record, with one violating directive per rule and one objector per record. In practice, each constraint is stated as an invariant record: a name, a property with a verb and its operand, the set it ranges over, the parties it binds, and the objector, which is either a gate check or none. A rule that holds only in a context is scoped by naming the context in its set, rather than by nesting a block. Each record is written so that a reviewer can point at a directive and say it broke this one, and the block sits after the last node.

To check this, write for each constraint the one directive that would violate it, and name the check that would notice. A constraint with no violating directive is an exhortation, and one with no objector is declared debt, which the record states with none. A constraint the model cannot observe from inside the document, such as a rule about its own confidence, cannot be checked by anything and belongs under [limits](https://banes-lab.com/pag/validation/limitations.md) rather than in an invariant record.

Scope is what keeps a constraint set small: a rule that holds everywhere is stated once, with every node as its set, and a rule that holds somewhere names where. The rewrite from exhortation to record is the same move every time. The operation and the operand are named, the adverb is dropped, and the objector is stated. Writing rules this way is [policy as code](https://banes-lab.com/records/arch/policy-as-code.md), and it lets a gate's check cite an invariant by name rather than restating it, as described in [orchestration invariants](https://banes-lab.com/pag/orchestration/orchestration-invariants.md).

Invariants close a document rather than open it, because they are read against the work they bind. A recovery block sits near the top, because recovery is a mechanism rather than a rule.

E1·a invariant records

```pag
# CROSS-NODE INVARIANTS · hold for every node, read after the nodes they bind · each a record with four slots
INVARIANT read-before-write: a surface is read whole before anything is persisted to it over: every persisting node binds: the reader objector: [check] a witness read precedes PERSIST_ARTIFACT
INVARIANT validate-at-boundary: an input is validated at every node boundary over: every node binds: the reader objector: [check] VALIDATE_ARTIFACT ran on the input at the gate
INVARIANT prior-output-only: a node reads only the prior node's output over: every node binds: the reader objector: [check] input names NODE n-1 or a slot
INVARIANT no-silent-blocker: a blocker stops the run until a decision is requested over: every node binds: the reader objector: [check] result routes unknown to BLOCKED
INVARIANT source-untouched: <source> is never modified in place over: <source> binds: the reader objector: none

# scoped · a property that holds only inside a named context is stated with that context in its set
INVARIANT encrypt-sensitive: every sensitive field is encrypted before it leaves the node over: nodes handling <sensitive-data> binds: the reader objector: [check] no plain-text field in the persisted artifact
INVARIANT audit-access: every access to <sensitive-data> is recorded over: nodes handling <sensitive-data> binds: the reader objector: [check] one audit entry per access
INVARIANT bounded-retention: <data> is not retained past <retention-period> over: persisted <data> binds: the reader objector: none
```

E1·b exhortation rewritten

```pag
# a rule the model can quote back · and a reviewer can check against a line
ALWAYS handle errors properly
INVARIANT wrapped-io: every <file-operation> runs inside TRY/CATCH over: file operations binds: the reader objector: [check] no bare file operation in the transform

NEVER do bad things
INVARIANT inside-the-root: nothing is persisted outside <workspace-root> over: every PERSIST_ARTIFACT binds: the reader objector: [check] every destination under the root

ALWAYS be careful with data
INVARIANT validated-first: <data> is validated against <schema> before it is processed over: every node reading <data> binds: the reader objector: [check] VALIDATE_ARTIFACT precedes the first use

NEVER modify anything
INVARIANT read-only-zone: no file in <read-only-zone> is modified over: <read-only-zone> binds: the reader objector: [check] a witness read of the zone after the run
```

E1·c worked document

```pag
---
name: <processor-name>
type: WORKFLOW
version: 1.0.0
---

THIS WORKFLOW EXECUTES validation and transformation of <records>

%% META %%:
objective: "An <output> whose entry count matches the conforming input"
jurisdiction: <source> and <output> | external: every other file
recursion_limit: 2

ON ERROR <write-failed>:
TRY:
RENAME <file> TO <file>.bak
PERSIST_ARTIFACT <content> TO <file>
DELETE <file>.bak
CATCH:
RENAME <file>.bak TO <file>

# NODE 1 — INPUT VALIDATION   [epistemic · analysis · logic · yields: boolean]
@genesis: existence
CONTRACT:
input:     <source>
transform: READ_RESOURCE <source> INTO <input>; FOR EACH <row> IN <input>.<rows>: VALIDATE_ARTIFACT <row> AGAINST <schema>; IF <row>.<conforms>: APPEND <row> TO <valid> ELSE: REPORT_RESULT "<which row, which field>"
output:    <valid>
HANDOFF GATE:
[check] <input> read (evidence: the read returned rows)
[check] every <row> validated (evidence: one verdict per row) over: <input>.<rows> measured: <validated> / <rows>
[check] every non-conforming <row> reported with its field (evidence: the report names a field per rejection)
result: pass → NODE 2 | unread → REPAIR (owner: NODE 1) | unknown → BLOCKED

# NODE 2 — TRANSFORMATION     [epistemic · formalisation · computation · yields: procedure]
@genesis: transformation
CONTRACT:
input:     <valid> from NODE 1
transform: FOR EACH <row> IN <valid>: COMPOSE_ARTIFACT <entry> FROM <row> USING <mapping>; APPEND <entry> TO <shaped>
preserves: the source row of every entry
output:    <shaped>
HANDOFF GATE:
[check] one <entry> per <row> in <valid> (evidence: the two counts match) over: <valid> measured: <shaped> / <rows>
[check] every <entry> conforms to <mapping> (evidence: VALIDATE_ARTIFACT passed on each)
[check] <valid> unchanged (evidence: a witness read after the transform)
result: pass → NODE 3 | mismatch → REPAIR (owner: NODE 2) | unknown → BLOCKED

# NODE 3 — OUTPUT             [evaluative · representation · information-theory · yields: artifact]
@genesis: emergence
CONTRACT:
input:     <shaped> from NODE 2
transform: PERSIST_ARTIFACT <shaped> TO <output>
output:    <output>
freshness: fingerprint(<shaped>) + fingerprint(this document)
HANDOFF GATE:
[check] <output> persisted (evidence: a read of <output> returns it)
[check] entry count of <output> matches <shaped> (evidence: the two counts match) over: <shaped> measured: <persisted> / <entries>
[check] <source> unchanged (evidence: a witness read)
refuse: <output> changed since it was read before PERSIST_ARTIFACT
result: pass → TERMINATE | loss → REPAIR (owner: NODE 3) | unknown → BLOCKED

# CROSS-NODE INVARIANTS
INVARIANT validate-before-transform: every row is validated before any transform reads it over: every node binds: the reader objector: [check] every <row> validated at NODE 1
INVARIANT reject-with-field: a rejected <row> is reported with its field over: rejected rows binds: the reader objector: [check] the report names a field per rejection at NODE 1
INVARIANT source-untouched: <source> is never modified over: <source> binds: the reader objector: [check] <source> unchanged at NODE 3
INVARIANT non-empty-input: the run does not proceed with zero conforming rows over: every run binds: the reader objector: [check] every <row> validated at NODE 1 over a non-empty set

REPORT:
subject: NODE 3
verdict: pass | fail | unknown
domain: declared <rows> measured <validated>
populations: conforming <n>, rejected <n>, persisted <n>
completion: saturated <bool> complete <bool> verified <bool>
```

E1·d exhortation or record

```mermaid
flowchart TB
rule["A rule"]
exhort["An exhortation · be careful, handle properly"]
judged["Judged by the model, differently each run"]
specific["A record · property, set, parties, objector"]
checkable["Checked by the objector, or declared unwatched"]
quoted["Quoted back by the model when it applies"]
rule --> exhort --> judged
rule --> specific --> checkable
specific --> quoted
```

E1·e four slots

```mermaid
flowchart LR
property["The property · could be false"]
set["over · the set it ranges over, the whole document or one context"]
parties["binds · who must receive it"]
objector["objector · what would disagree, or none"]
property --> set --> parties --> objector
```

## Links to

- [The loop](https://banes-lab.com/disciplined-methodology/start/the-loop.md)
- [Limits](https://banes-lab.com/pag/validation/limitations.md)
- [Policy as Code](https://banes-lab.com/records/arch/policy-as-code.md)
- [Orchestration invariants](https://banes-lab.com/pag/orchestration/orchestration-invariants.md)
