Skip to content

Latest commit

 

History

History

README.md

pawl/contract/ — Cross-lane contracts

This package houses shared contracts consumed by multiple lanes (forward, reverse, normalize): stable types and lane-neutral capabilities.

Contents

  • values.pyValue union (ValueString, ValueRegex, ValueBool, ValueNumber, ValueSymbol, ValueParamRef) and NodeId.
  • policy_dag.pyPolicy DAG type inventory (FilterAtom, NodeTest/NodeAllow/NodeDeny/NodeJumpOp, OperationDAG, Policy).
  • coordinates.py — envelope bridge types (CoordinateIndex, HashBoundaries) and coordinate builder helpers.
  • envelope.pyIREnvelope assembly plus Track D.2 structural gate validation (validate_structural_gate_d2, assert_structural_gate_d2).
  • bundle_ir.py — canonical multi-profile BundleIR representation and Track F bundle structural gates (validate_bundle_ir, assert_bundle_ir).
  • render.py — semantic renderer (render_policy) for Policy -> SBPL without reverse-lane fidelity heuristics.

Import convention

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_policy

Existing 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.

What belongs here

  • 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.

Two renderers and the scope boundary

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.

Semantic renderer (pawl/contract/render.py)

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.

Roundtrip renderer (pawl/reverse/render/sbpl.py)

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.

When to use which

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.