This package houses shared contracts consumed by multiple lanes (forward, reverse, normalize): stable types and lane-neutral capabilities.
values.py—Valueunion (ValueString,ValueRegex,ValueBool,ValueNumber,ValueSymbol,ValueParamRef) andNodeId.policy_dag.py—PolicyDAG type inventory (FilterAtom,NodeTest/NodeAllow/NodeDeny/NodeJumpOp,OperationDAG,Policy).coordinates.py— envelope bridge types (CoordinateIndex,HashBoundaries) and coordinate builder helpers.envelope.py—IREnvelopeassembly plus Track D.2 structural gate validation (validate_structural_gate_d2,assert_structural_gate_d2).bundle_ir.py— canonical multi-profileBundleIRrepresentation and Track F bundle structural gates (validate_bundle_ir,assert_bundle_ir).render.py— semantic renderer (render_policy) forPolicy -> SBPLwithout reverse-lane fidelity heuristics.
New code should import from pawl.contract modules directly:
from pawl.contract.values import Value, ValueString, NodeId
from pawl.contract.policy_dag import Policy, OperationDAG
from pawl.contract.render import render_policyExisting forward-lane code may continue importing from pawl.forward.layer2.ir,
which re-exports these types for backward compatibility. That re-export path is
not intended for new consumers.
- A type moves here when concrete code in a second lane needs it.
- A function moves here when it is lane-neutral and must be shared across lanes.
- Do not move speculative surfaces; require real two-lane production demand.
There are two SBPL renderers. They serve different goals and depend on different inputs. Understanding the boundary between them is essential for knowing what each one can and cannot produce.
render_policy(policy: Policy) -> str takes a Policy DAG from any source
(forward compile-decode, forward source-evaluate, reverse DAG emitter, or
hand-constructed) and produces compilable SBPL text.
It works entirely from the DAG's typed content: filter names, Value-typed
arguments, and graph topology (NodeTest match/unmatch edges). It
reconstructs require-any, require-all, and require-not groupings from
binary decision chains via boolean expression algebra.
What it handles:
| Input | Output |
|---|---|
NodeTest chains with shared unmatch targets |
(require-any ...) |
NodeTest chains where all unmatch branches converge to deny |
(require-all ...) |
NodeTest with inverted polarity (match→deny, unmatch→allow) |
(require-not ...) |
| Mixed nesting of the above | Nested SBPL grouping forms |
NodeJumpOp |
Semantic delegation to the target operation's DAG |
NodeAllow/NodeDeny at entry (terminal-only) |
(allow op) / (deny op) |
NodeUnknown / NodeUnknownInlinePolicyRef |
SBPL comment warnings |
Multi-arg FilterAtom |
(require-any (filter arg1) (filter arg2)) |
Empty-arg FilterAtom |
(filter-name) |
What it does not handle — and why: The semantic renderer intentionally
does not implement any logic that depends on reverse-lane metadata the Policy
DAG does not carry. These concerns belong exclusively to the roundtrip
renderer (pawl/reverse/render/sbpl.py):
| Concern | Why it is not here | What metadata it needs |
|---|---|---|
| Profile-type baseline suppression | The compiler silently inserts operations that don't appear in source SBPL. Suppressing them requires knowing what the compiler would insert for a bare default. | profile_type_baseline.v1.json (derived from live compiler behavior) |
| Contextual implicit suppression | The compiler promotes certain operations when a profile contains filter rules. Suppressing them requires corpus-derived evidence of promotion patterns. | contextual_implicits.v1.json (learned from compiled corpus) |
| Wildcard sub-operation suppression | When a wildcard operation (e.g. file*) and its child (e.g. file-read*) have equivalent subtrees, only the wildcard should be emitted. Detection needs subtree signatures over the full op-table. |
Op-table structure, graph signatures |
| Message-filter reconstruction | Compiled blobs encode message-filter rules as terminal marker nodes with message_filter_index. Reconstructing (apply-message-filter ...) forms requires this index. |
message_filter_index from parsed terminal nodes |
| Pool-position reordering | The compiler serializes predicates in literal-pool order, not source order. Restoring source order requires pool byte offsets. | Literal pool byte offsets |
| Predicate recovery | Operation-conditioned collapse of invalid predicates (from compiler node-sharing contamination) requires oracle confidence levels. | ReducedGraph + predicate merge oracle |
| Unsupported-string stripping | The compiler generates broken literal siblings alongside regex nodes. Removing them requires detecting the compiler's pattern. | Raw node bytes, sibling structure |
The semantic renderer is gap-free by construction: it faithfully renders
whatever the DAG contains. The quality of its output is bounded by what the
DAG emitter produces (reverse lane: pawl/reverse/core/dag_emitter.py;
forward lane: pawl/forward/strategies/source_evaluate.py and
compile_decode.py). If an emitter produces a correct DAG, render_policy
produces correct SBPL.
render_profile_sbpl(...) takes reverse-lane operation nodes (with full
parsed metadata) and produces SBPL text that can be recompiled to a
structurally equivalent blob. It applies the suppression and fidelity controls
listed above. See pawl/reverse/render/README.md for details.
Use render_policy when you have a Policy DAG and need compilable SBPL
text — the common case for IR consumers, contract tests, and any path that
starts from a DAG regardless of which lane produced it.
Use render_profile_sbpl when you need blob-faithful roundtrip output and
the reverse lane's suppression/recovery logic. This is the path for casefile
reverse artifacts and reverse→compile fidelity checks.