wardline 02 A python binding
Part II-A: Python Language Binding Reference¶
This section provides the Python-specific binding reference for the Wardline framework specification (Part I). It covers the annotation vocabulary, interface contract, enforcement mechanisms, and residual risks specific to Python. The parent specification (Part I) governs; this binding implements.
Normative status. Section A.3 (interface contract) is normative. All other sections are non-normative — they provide design rationale, worked examples, and implementation guidance.
A.1 Design history¶
This section is non-normative.
The wardline concept emerged through three iterations of structured adversarial deliberation using prompted AI agent teams. Iteration 1 asked whether Python's permissive defaults could be addressed at the language level (a stricter dialect, a transpiler, or a runtime enforcement layer). An agent team concluded unanimously that the approach was not viable — ecosystem orphaning, the adoption cliff, and the maintenance burden were each independently fatal.
Iteration 2 — operating with knowledge of the first team's conclusions but no other constraint — independently generated the concept that became the semantic boundary enforcer: a standalone AST-based analysis layer that extends Python's existing annotation machinery rather than creating a parallel system. The creative pivot — from "change the language" to "analyse the language" — was generated by the agent team, not by the human operator. This team's design was implemented as a pattern-matching enforcement gate and deployed in production on the case study codebase.
Iteration 3 used seven specialist agent perspectives to refine the design. Binary taint tracking was rejected by all seven agents; the team replaced it with the two-dimensional model tracking trust classification and validation status as orthogonal dimensions — now formalised in the parent specification (§6).
Feasibility finding. The deployed predecessor validates the approach at the pattern-matching level: automated detection of common agentic code failure modes is technically feasible for Python, compatible with existing development workflows, and buildable at modest cost relative to typical internal tooling.
Posture: reference implementation, not product. The Python enforcement regime described in this document is not a single product. It is a composition of existing ecosystem tools — ruff for syntactic pattern detection, mypy for type-layer tier diagnostics — and a reference implementation that covers the analysis surface no existing tool addresses: tier-aware taint-flow tracking, structural verification, and governance orchestration. The specification is the durable artefact. The reference implementation proves the specification is implementable. But the specification is designed so that any tool author can implement a compatible Wardline-Core scanner, Wardline-Type plugin, or Wardline-Governance orchestrator independently. The regime matures when the specification is owned and evolved independently of any single implementation.
A.2 Python language evaluation¶
This section is non-normative. It models how future binding authors should assess their language against Part I §12's evaluation criteria.
| Criterion | Assessment | Detail |
|---|---|---|
| Annotation expressiveness | Strong | Decorators can express all 17 annotation groups. Decorators are metadata-only, visible to ast.parse() without execution, and compose naturally. |
| Parse tree access | Strong | The ast standard library module ships with every CPython install. Zero external dependencies for full AST analysis. |
| Type system metadata | Moderate | typing.Annotated (Python 3.9+) enables field-level tier marking. However, type hints are optional and not enforced at runtime. |
| Structural typing | Moderate | typing.Protocol (Python 3.8+) enables structural subtyping. @runtime_checkable Protocols support isinstance() checks. However, Protocols are not widely adopted for trust semantics, and mypy adoption remains optional. |
| Runtime object model | Strong | Descriptors (__get__/__set__/__set_name__), __init_subclass__, and metaclasses provide rich runtime structural enforcement. Standard CPython machinery, not extensions. |
| Class hierarchy enforcement | Strong | __init_subclass__ fires at class definition time (import time), not at instance creation time. Violations caught at module load. |
| Serialisation boundary control | Weak | Static analysis cannot cross serialisation boundaries. json.loads() returns dict regardless of what was serialised. Descriptors provide partial runtime coverage, but the fundamental blind spot remains. |
| Error model | Strong | Python's exception model provides a rich and explicit detection surface for WL-003, WL-004, and WL-005. raise, except, cause chaining, and broad-catch idioms are all visible in the AST. |
| Concurrency model | Moderate | Python combines threads, asyncio, multiprocessing, and callback-heavy frameworks. The GIL narrows some shared-memory races but does not eliminate ordering or coordination hazards; Group 13 reasoning remains binding-defined and context-sensitive. |
| Tooling ecosystem | Strong | mypy, pyright, ruff, bandit, and extensive AST tooling. SARIF is the standard interchange format. |
- Python: Language evaluation criteria for wardline binding suitability
A.2.1 Where Python falls short¶
Three significant limitations shape this language binding:
No linear types. Python cannot prevent aliasing of validated data. A ValidatedRecord can be assigned to multiple variables, passed to functions that mutate it, or stored in containers that mix tiers — and the type system cannot track which aliases are still valid. Languages with ownership models (Rust) or linear types can prevent this at the type level; Python cannot.
No compile-time enforcement. Type checking in Python is optional. A codebase can use typing.Annotated for tier marking and Protocol for trust-typed signatures, but nothing prevents a developer (or agent) from ignoring these annotations entirely. The AST scanner compensates by checking decorator contracts at CI time, but there is a gap between authoring time and CI feedback.
No ownership model. Python cannot prevent lower-tier data from being aliased as Tier 1 at runtime without active enforcement mechanisms (descriptors, runtime checks). In a language with ownership semantics, a tier promotion would consume the original value, making it structurally impossible to reference the pre-promotion data.
These limitations define the ceiling of assurance achievable through this language binding. Adopters should understand that Python's assurance ceiling is lower than what a language with ownership semantics and mandatory type checking could provide.
A.2.2 Ecosystem tool coverage¶
| Conformance Profile | Candidate Tool | Implementation Path | Fit |
|---|---|---|---|
| Advisory fast path (non-conformant) | ruff | Custom rule plugin — syntactic pattern matching for PY-WL-001 through PY-WL-005 | Strong — ruff's per-file AST rule architecture matches pattern detection directly. Advisory only: no manifest, no tier-graded SARIF |
| Wardline-Core (authoritative) | Bespoke scanner | Two-pass AST analysis with taint tracking, manifest consumption, SARIF output | Required — no existing tool provides tier-aware severity grading |
| Wardline-Type | mypy plugin | Plugin using mypy's type analysis hooks to understand Annotated tier metadata | Strong — mypy's plugin API supports custom type metadata and diagnostic hooks |
| Wardline-Type (baseline) | pyright (no plugin) | Standard typing.Protocol and typing.Annotated mechanisms | Moderate — structural conformance but no tier-flow analysis |
| Wardline-Governance | Bespoke CLI | Manifest validation, fingerprint baseline, SARIF aggregation, control-law reporting | Required — governance orchestration is wardline-specific |
- Python: Ecosystem tool coverage for conformance profiles
A.3 Interface contract (Wardline-Core)¶
This section is normative.
Any tool that implements Wardline-Core rules for the Python regime — whether a ruff plugin, the reference scanner, a Semgrep rule pack, or a future competing implementation — MUST satisfy the following interface contract:
-
Manifest consumption. The tool MUST consume the wardline manifest (
wardline.yamland any overlays) and validate the manifest against the framework's JSON Schemas before producing findings. A tool that produces findings without validating the manifest is non-conformant. (Note:wardline.yamlis the trust topology manifest.wardline.tomlis the scanner's operational configuration. The two files serve different purposes.) -
Decorator discovery. The tool MUST discover wardline decorator syntax from the target codebase's AST — identifying which functions carry which decorators and extracting their arguments. The tool SHALL NOT rely on runtime introspection or dynamic attribute inspection as the primary discovery mechanism. The Python decorator vocabulary and its argument schemas are defined in §A.4. Cross-binding machine identity remains the Part I annotation-group numbering and manifest schema identifiers, not the Python decorator spellings.
-
Schema default recognition. The tool MUST recognise
schema_default()as a PY-WL-001 suppression marker. Calls wrapped inschema_default()where the default value matches the overlay's declared approved default are governed by the overlay declaration, not by PY-WL-001. -
SARIF output. The tool MUST produce findings in SARIF v2.1.0 with the wardline-specific property bags defined in the parent specification (§11.1). The Python regime requires the following mandatory property bag keys on each
resultobject:Key Type Description wardline.rulestring Binding rule ID (e.g., PY-WL-001)wardline.taintStatestring Canonical taint state token (e.g., INTEGRAL)wardline.severitystring ERROR,WARNING, orSUPPRESSwardline.exceptionabilitystring UNCONDITIONAL,STANDARD,RELAXED, orTRANSPARENTwardline.analysisLevelinteger Analysis level that produced the finding (1, 2, or 3) - Python: Mandatory SARIF property bag keys for Wardline-Core result objects
wardline.analysisLevelvalues are defined as follows:1— minimum conformant static analysis: intraprocedural rule detection plus the framework's required two-hop scope for WL-007 delegation and taint-flow through unannotated intermediaries2— level 1 plus variable-level taint tracking within function bodies and container-sensitive propagation beyond whole-function approximation3— level 2 plus transitive interprocedural inference beyond the required two-hop minimum
A tool MUST emit the lowest level whose capabilities are sufficient to justify the finding as produced. Tools that implement only the framework minimum emit
wardline.analysisLevel: 1for all findings.
Analysis Level 1 — Minimum Conformant Scope¶
Level 1 is the Python binding's minimum conformant static-analysis claim. At this level, the scanner MAY rely on bounded project-local resolution, but it MUST stay within the published minimum: direct flows plus one undecorated intermediary hop. That minimum applies both to explicit-flow taint between declared boundaries and to the delegated rejection checks that Part I permits within the two-hop scope. Level 1 does not claim SCC iteration, fixed-point convergence, arbitrary transitive interprocedural inference, or deep dynamic-dispatch recovery.
Analysis Level 3 — Transitive Interprocedural Scope¶
Level 3 is the first Python binding level that claims transitive interprocedural inference beyond the bounded minimum. This is where SCC decomposition, fixed-point refinement, and broader project-wide call-graph reasoning belong. A tool MUST NOT describe bounded two-hop analysis as level 3 merely because it spans files, and it MUST NOT describe level 1 as transitive interprocedural analysis.
-
Decorator composition. The tool MUST resolve decorator stacking on the same function. In particular,
@int_datawithout@restoration_boundaryproducesUNKNOWN_RAW;@int_datacomposed with@restoration_boundaryproduces the effective tier determined by the Part I §6.3 evidence matrix. -
Third-party delegation resolution. For PY-WL-008 / framework WL-007 delegation analysis, the tool SHOULD resolve calls into installed package source where that source is available to static analysis. Where installed source is unavailable or resolution is not implemented, the tool MUST treat the delegation as unresolvable for minimum-conformance purposes and evaluate the boundary conservatively.
-
Rule declaration. The tool MUST declare which rules it implements and MUST maintain golden corpus specimens for those rules.
-
Verification mode. The tool MUST support the
--verification-modeoutput profile for deterministic byte-identical output against the golden corpus (Part I §11, property 5). -
Run-level SARIF properties. In addition to the mandatory result-level properties above, the tool MUST emit the required run-level SARIF properties defined in Part I §11.1 for Wardline-Core tools, including
wardline.inputHash,wardline.inputFiles,wardline.manifestHash, andwardline.controlLaw.
A tool that satisfies this contract and implements at least one PY-WL rule is a partial Wardline-Core tool. A tool that implements all ten binding rules (PY-WL-001 through PY-WL-010) with tier-aware severity grading is a complete Wardline-Core tool. Note: PY-WL-001 and PY-WL-002 both derive from framework rule WL-001 (split by access idiom). PY-WL-003 derives from framework WL-002, and the binding numbering is offset by two from PY-WL-003 onward (WL-001 splits into PY-WL-001/002).
Rule mapping. The ten Python binding rules derive from the nine framework rules (Part I §8) as follows. WL-001 splits into two binding rules because Python has two distinct access-with-fallback idioms (dict.get() and getattr()); all other framework rules map one-to-one with a numbering offset:
| Python Rule | Framework Rule | Pattern |
|---|---|---|
| PY-WL-001 | WL-001 (split) | Dict key access with fallback default (.get(), .setdefault(), collections.defaultdict) |
| PY-WL-002 | WL-001 (split) | Attribute access with fallback default (getattr() with default) |
| PY-WL-003 | WL-002 | Existence-checking as structural gate (if key in dict, hasattr() guards as validation proxy) |
| PY-WL-004 | WL-003 | Broad exception handlers swallowing errors (except Exception, bare except) |
| PY-WL-005 | WL-004 | Catching exceptions silently — no action taken in handler (except: pass, bare except with no re-raise or logging) |
| PY-WL-006 | WL-005 | Audit-critical writes inside broad exception handlers |
| PY-WL-007 | WL-006 | Runtime type-checking internal data (tier-dependent — suppressed at Tier 4) |
| PY-WL-008 | WL-007 | Validation boundary with no rejection path (structural verification) |
| PY-WL-009 | WL-008 | Semantic validation without prior shape validation (validation ordering) |
| PY-WL-010 | WL-009 | Tier 1 promotion on serialisation path without restoration evidence (restoration symmetry) |
- Python: Mapping of binding rules to framework rules
PY-WL-001 through PY-WL-005 are syntactic patterns detectable by per-file AST analysis (the ruff advisory path). PY-WL-006 through PY-WL-010 require semantic context — audit-path awareness, tier classification, structural verification, validation ordering, or restoration symmetry — and are implemented by the reference scanner.
Tools that detect wardline-relevant patterns without satisfying this contract — e.g., ruff rules that match .get() calls without consuming the manifest — are advisory tools, not Wardline-Core tools. Advisory tools provide useful early-warning feedback but their findings are not governance-grade.
A.4 Annotation vocabulary: design principles, mapping table, and rationale¶
This section is non-normative except where explicitly stated.
A.4.1 Design principles¶
Parasitic, not parallel. The decorators extend Python's existing machinery. They are standard decorators importable from a PyPI package. No custom syntax, no runtime overhead beyond attribute assignment, no framework lock-in.
Sparse annotation, dense inference. Developers annotate boundaries — where trust changes, where failure modes matter, where ordering is required. The scanner infers everything between boundaries. Target: ~50–100 decorators for an 80k-line codebase in the initial annotation pass.
Library, not framework. The decorator vocabulary is a reusable PyPI package. Projects pick what they need via wardline.toml configuration — unused decorator groups are ignored by the scanner.
Decorators as machine-readable institutional knowledge. Each decorator converts a prose-level institutional constraint ("audit records must not have fabricated defaults") into a machine-checkable declaration.
Coding posture per tier. The parent specification's authority tier model (§5) implies distinct programming styles:
| Tier | Posture | Philosophy |
|---|---|---|
| Tier 4 | Untrusted-input (sceptical) programming | Treat everything as hostile sludge. Validate structure first, normalise, reject. |
| Tier 3 | Structure-verified (guarded) programming | Structure is trustworthy. Direct field access is safe; validate domain constraints before using values in business logic. |
| Tier 2 | Governance-assured (confident) programming | Structure and domain meaning are trustworthy within the declared validation scope. Guard only cross-cutting concerns. |
| Tier 1 | Strict (offensive) programming (assert invariants; never silently recover) | Assume invariants, detonate on breach. Anomalies must surface immediately as faults. |
- Python: Coding posture and philosophy by authority tier
Minimum Python version: 3.12+. The scanner targets Python 3.12+ only. ast.Constant is the canonical node at this floor; ast.Match (3.10+) and ast.unparse() (3.9+) are available.
A.4.2 Decorator mapping table¶
The 17 annotation groups are defined as language-agnostic semantic requirements in Part I §7. This table provides the Python-specific decorator syntax. Decorators set _wardline_* metadata attributes on the decorated callable; they do almost nothing at runtime. In SARIF and other cross-binding interchange, annotation context is identified by Part I group numbers (wardline.annotationGroups), while Python decorator names remain binding-specific diagnostic detail.
| # | Group | Python Decorator(s) | Signature / Parameters | Scanner Checks |
|---|---|---|---|---|
| 1 | Authority Tier Flow | @external_boundary | (none) | Return value tagged TIER_4. Auto-detected for known external call sites but explicit annotation preferred. |
| 1 | @validates_shape | (none) | Absence of rejection path in body produces a finding (WL-007). T4 → T3 constructor. | |
| 1 | @validates_semantic | (none) | Absence of rejection path in body produces a finding (WL-007). Inputs that do not trace to @validates_shape output produce a finding (WL-008/PY-WL-009). T3 → T2 constructor. Validation scope declared in the overlay per Part I §14.1.2. | |
| 1 | @validates_external | (none) | Combined T4 → T2. Absence of rejection path in body produces a finding (WL-007). The scanner verifies that the body performs both structural and semantic checks (§6.2). | |
| 1 | @integral_read | (none) | Body bans: .get() with defaults, getattr() with fallbacks, hasattr(), broad except. Return tagged TIER_1. | |
| 1 | @integral_writer | (none) | Call-site bans: enclosing swallowing except. Audit call MUST dominate telemetry on shared execution paths. Violation produces a finding. Fallback paths that bypass the audit call produce a finding. Return tagged TIER_1. | |
| 1 | @integral_construction | (none) | Same body restrictions as @integral_read. Semantically equivalent to @integral_writer but for non-audit authoritative artefacts. Return tagged TIER_1. | |
| 2 | Integrity Primacy | @integrity_critical | (none) | Superset of @integral_writer — fallback paths at call sites that skip the audit call produce a finding. |
| 3 | Plugin Contract | @system_plugin | (none) | Body bans top-level broad except. Allows narrower except for external calls and row-value operations. "Wrap your external calls; let your own bugs crash." |
| 4 | Data Provenance | @int_data | (none) | Tier 1 body restrictions. Return value is UNKNOWN_RAW unless composed with @restoration_boundary. Allow-list/deny-list on call targets. |
| 5 | Schema Contracts | @all_fields_mapped(source=Class) | source: the class whose fields are verified to all appear in the body | Verifies every field on source appears as attribute access on the parameter. |
| 5 | @output_schema(fields=[...]) | fields: list of output field names | Field collision detection at call sites. | |
| 5 | (access-site) | schema_default(expr) | Wraps a .get() expression | Suppression marker for PY-WL-001. Scanner verifies overlay declaration, default value match, and validation boundary context. Part of Wardline-Core interface contract. |
| 6 | Layer Boundaries | @layer(N) | N: integer layer number | Import direction enforcement. Upward imports are findings. Hybrid: default layer from directory path via wardline.toml, decorator overrides per symbol. |
| 7 | Template Safety | @parse_at_init | (none) | Call sites outside __init__, __post_init__, or setup methods produce a finding. Calls from per-row methods are findings. |
| 8 | Secret Handling | @handles_secrets | (none) | Return tagged SECRET (orthogonal taint dimension). SECRET reaching logger, print, persistence without hashing is a finding. |
| 9 | Operation Semantics | @idempotent | (none) | First state-modifying call not preceded by existence/dedup guard produces a finding. |
| 9 | @atomic | (none) | Multiple state-modifying calls outside transaction context produce a finding. | |
| 9 | @compensatable(rollback=fn) | rollback: reference to rollback function | Scanner verifies rollback function exists with compatible signature. | |
| 10 | Failure Mode | @fail_closed | (none) | Same body restrictions as @integral_read. Carries implicit @must_propagate. Severity lookups use INTEGRAL. |
| 10 | @fail_open | (none) | Explicitly permits graceful degradation patterns. Composition requirement: absence of a trust classification decorator produces a WARNING. | |
| 10 | @emits_or_explains | (none) | Return/exit paths that do not reach an emit call or an explain/logging call produce a finding. | |
| 10 | @exception_boundary | (none) | Authorises exception handling from high-stakes call sites. Still subject to PY-WL-005. Placement governed via wardline.toml (lenient/controlled/strict modes). | |
| 10 | @must_propagate | (none) | Exceptions that do not propagate to an @exception_boundary produce a finding. No intermediate catch-and-continue. | |
| 10 | @preserve_cause | (none) | A raise X(...) in an except block without a from clause produces a finding. One-hop traversal into helper calls. | |
| 11 | Data Sensitivity | @handles_pii(fields=[...]) | fields: list of PII field names | Named fields reaching logger, error messages, or unprotected persistence produce a finding. |
| 11 | @handles_classified(level=str) | level: classification level (e.g., "PROTECTED") | No mixing with lower classification levels. No downgrading without @declassifies. | |
| 11 | @declassifies(from_level=str, to_level=str) | from_level, to_level: classification levels | Absence of rejection path in body produces a finding. CODEOWNERS-protected. | |
| 12 | Determinism | @deterministic | (none) | Body ban on non-deterministic stdlib calls (random, uuid4, datetime.now, set iteration). |
| 12 | @time_dependent | (none) | Suppresses @deterministic-style findings. | |
| 13 | Concurrency/Ordering | @thread_safe | (none) | Body that does not protect shared mutable state and is not pure produces a finding. v1.0: not enforced — advisory declaration only. |
| 13 | @ordered_after(name) | name: function name that is verified to precede | At call sites where both functions appear, the named function not lexically preceding produces a finding. v1.0: enforced via SUP-001. | |
| 13 | @not_reentrant | (none) | Call graph cycle detection through the decorated function. v1.0: enforced via SUP-001. | |
| 14 | Access/Attribution | @requires_identity | (none) | Absence of identity-typed parameter in @integral_writer/@integrity_critical call within body produces a finding. |
| 14 | @privileged_operation | (none) | State-modifying call not preceded by authorisation check produces a finding. | |
| 15 | Lifecycle/Scope | @test_only | (none) | Import of this symbol from a production module produces a finding. |
| 15 | @deprecated_by(date=str, replacement=str) | date: expiry date; replacement: replacement function | Post-expiry: blocking. Pre-expiry: advisory. | |
| 15 | @feature_gated(flag=str) | flag: feature flag name | Static reference counting; stale flag detection. | |
| 16 | Generic Trust Boundary | @trust_boundary(from_tier=N, to_tier=M) | from_tier, to_tier: integers 1–4 | Parameterised tier transition. Promotion requires rejection path. Skip-promotions to T1 are schema-invalid. |
| 16 | @data_flow(consumes=N, produces=M) | consumes, produces: integers 1–4 | Descriptive-only documentation marker. No enforcement. Advisory if produces > consumes. | |
| 17 | Restoration Boundaries | @restoration_boundary(...) | restored_tier: int; institutional_provenance: str (opt); structural_evidence: bool; semantic_evidence: bool (opt); integrity_evidence: str (opt) | Body that does not satisfy WL-007 produces a finding. Evidence that does not support the claimed tier per §6.3 evidence matrix produces a finding. Scanner demotes effective taint state when evidence is insufficient. |
- Python: Decorator vocabulary mapping for 17 annotation groups
Group 1 aliases and Group 16 equivalences. Group 1 decorators are convenience aliases for common Group 16 configurations:
| Group 1 Decorator | Equivalent Group 16 | Notes |
|---|---|---|
@external_boundary | Sources TIER_4 (no from_tier) | |
@validates_shape | @trust_boundary(from_tier=4, to_tier=3) | |
@validates_semantic | @trust_boundary(from_tier=3, to_tier=2) | |
@validates_external | @trust_boundary(from_tier=4, to_tier=2) | |
@integral_read | Sources TIER_1 (no from_tier) | |
@integral_writer | @trust_boundary(from_tier=2, to_tier=1) + call-site enforcement | Audit-ordering semantics not expressible via @trust_boundary alone |
@integral_construction | @trust_boundary(from_tier=2, to_tier=1) |
- Python: Group 1 decorator aliases to Group 16 equivalents
A.4.3 Non-obvious design rationale¶
Why Python uses decorator stacking. Python decorators compose naturally via stacking — @int_data above @restoration_boundary produces a function with both Group 4 body restrictions and Group 17 evidence verification. This is standard Python idiom. Each decorator sets its own _wardline_* metadata attributes independently. The scanner reads all attributes and applies each group's rules. No decorator needs awareness of the others in the stack. In normative terms (§A.3), @int_data alone yields UNKNOWN_RAW; only composition with @restoration_boundary can restore a higher effective tier, and then only to the level supported by the declared evidence.
Which groups share decorators and why. Groups 1 and 16 share the tier-transition concept — Group 1 decorators are aliases for common Group 16 configurations. This means @validates_shape and @trust_boundary(from_tier=4, to_tier=3) are semantically identical. The aliases exist for readability: most codebases use the Group 1 names; Group 16 exists for non-standard transitions. Groups 8 and 11 share the sensitivity.py module because both deal with data sensitivity (secrets vs. classification levels) and use the same taint propagation engine with different taint dimensions. Groups 9 and 10 share operations.py because both deal with function-level behavioural contracts.
functools.wraps preservation. When a wardline decorator is applied to a function, _wardline_* metadata attributes are set on the wrapper function. If the function is subsequently wrapped by another decorator that uses functools.wraps, the metadata attributes are preserved because functools.wraps copies the __wrapped__ attribute and updates __dict__. The scanner resolves decorated functions through __wrapped__ chains to find wardline metadata, ensuring decorators from third-party libraries (e.g., Flask's @route, pytest's @fixture) do not hide wardline annotations.
Metaclass and descriptor implications (from §21). Python's descriptor protocol (__get__/__set__/__set_name__) provides runtime structural enforcement that complements the static analysis layer. The key design decision: AuthoritativeField descriptors raise on access-before-set, making fabricated defaults structurally impossible for Tier 1 data at the Python object model level. This catches violations that the AST scanner cannot reach (dynamic dispatch, cross-module indirection, generated code). The limitation: __dict__ manipulation bypasses the descriptor's __set__ sentinel check — a fundamental constraint of Python's descriptor protocol. Similarly, __init_subclass__ fires at class definition time (import time) to enforce that subclass methods carry wardline decorators, but composition-based delegation (creating unannotated helper classes within annotated method bodies) bypasses this enforcement.
How @validates_external relates to the decomposed validators. @validates_external (combined T4→T2) performs both shape and semantic validation in a single function body. The model treats this as two logical transitions (T4→T3→T2) occurring within one function. The scanner establishes that the body performs both structural and semantic checks (§6.2 invariant 3). The decomposed form (@validates_shape + @validates_semantic on separate functions) is preferred for large validators where the structural and semantic concerns are distinct. The combined form is appropriate when both checks are simple enough to co-locate without confusion. Stacking @validates_shape + @validates_semantic on the same function is contradictory (SCN-021) — use @validates_external for the combined case.
Body evaluation context for validation boundaries. At the first analysis level, the scanner evaluates pattern rules within validation boundary bodies using the severity lookups of the input tier — the tier the validator operates on, not the tier it produces:
| Decorator | Input tier | Body evaluation severity | PY-WL-003 (existence-checking) suppression |
|---|---|---|---|
@validates_shape | TIER_4 | EXTERNAL_RAW | Yes — existence-checking is the purpose of shape validation |
@validates_semantic | TIER_3 | GUARDED | No — structural guarantees already established |
@validates_external | TIER_4 | EXTERNAL_RAW | Yes — encompasses shape validation |
- Python: Body evaluation context for validation boundary decorators
@fail_closed strictness ordering. When decorators compose, severity dominates exceptionability: ERROR > WARNING > SUPPRESS regardless of exceptionability class. Within the same severity, exceptionability is ordered UNCONDITIONAL > STANDARD > RELAXED. When @fail_closed is composed with a validation boundary decorator, body pattern rules fire at ERROR severity but exceptionability is capped at STANDARD — the findings remain governable. UNCONDITIONAL exceptionability is reserved for INTEGRAL-native contexts.
Exception translation boundaries. The failure mode decorators (@fail_closed, @fail_open, @emits_or_explains) govern what happens within a function. The exception propagation decorators (@exception_boundary, @must_propagate, @preserve_cause) govern what happens to exceptions after they leave the function. Without @exception_boundary, a @fail_closed function correctly raises on failure — and then a caller three frames up catches with except Exception: use_default(), defeating the @fail_closed intent entirely. @exception_boundary declares which functions are architecturally authorised to make terminal policy decisions about exceptions from high-stakes paths. @must_propagate carries implicit from @fail_closed and @integrity_critical.
Contradictory combination detection (SCN-021). The scanner detects mutually exclusive decorator combinations as ERROR findings. The 29 detected combinations (26 contradictory, 3 suspicious) are:
| # | Combination | Type | Rationale |
|---|---|---|---|
| 1 | @fail_open + @fail_closed | Contradictory | Mutually exclusive failure modes |
| 2 | @fail_open + @integral_read | Contradictory | Tier 1 requires strict (offensive) programming — fail-open is structurally incompatible |
| 3 | @fail_open + @integral_writer | Contradictory | Audit writes must not silently degrade |
| 4 | @fail_open + @integral_construction | Contradictory | Authoritative artefacts must not have fallback construction paths |
| 5 | @fail_open + @integrity_critical | Contradictory | Audit-critical paths must not have fallback paths |
| 6 | @external_boundary + @int_data | Contradictory | External and internal data sources are mutually exclusive |
| 7 | @external_boundary + @integral_read | Contradictory | External data is Tier 4; Tier 1 reads are internal |
| 8 | @external_boundary + @integral_construction | Contradictory | External data cannot be directly authoritative |
| 9 | @validates_shape + @validates_semantic | Contradictory | Use @validates_external for combined T4→T2 |
| 10 | @validates_shape + @integral_read | Contradictory | Shape validation produces T3, not T1 |
| 11 | @validates_semantic + @external_boundary | Contradictory | Semantic validation operates on T3 input, not T4 |
| 12 | @exception_boundary + @must_propagate | Contradictory | Exception boundaries terminate; must-propagate requires forwarding |
| 13 | @idempotent + @compensatable | Contradictory | Idempotent operations need no compensation |
| 14 | @deterministic + @time_dependent | Contradictory | Time-dependent operations are inherently non-deterministic |
| 15 | @deterministic + @external_boundary | Contradictory | External calls are non-deterministic by definition |
| 16 | @integral_read + @restoration_boundary | Contradictory | Tier 1 reads access existing authoritative data; restoration reconstructs from raw representation |
| 17 | @integral_writer + @restoration_boundary | Contradictory | Audit writes create new records; restoration reconstructs existing ones |
| 18 | @fail_closed + @emits_or_explains | Contradictory | Fail-closed raises on failure; emits-or-explains requires structured error output |
| 19 | @integrity_critical + @fail_open | Contradictory | (Alias of #5 — caught regardless of decorator ordering) |
| 20 | @validates_external + @validates_shape | Contradictory | @validates_external already encompasses shape validation |
| 21 | @validates_external + @validates_semantic | Contradictory | @validates_external already encompasses semantic validation |
| 22 | @int_data + @validates_shape | Contradictory | Internal data does not need shape validation (already T1) |
| 23 | @preserve_cause + @exception_boundary | Contradictory | (Alias of #12 — @preserve_cause implies propagation) |
| 24 | @compensatable + @integral_writer | Contradictory | Audit writes must not be compensated (reversed) |
| 25 | @data_flow(produces=...) + @external_boundary | Contradictory | External boundaries produce T4 data; data-flow produces declared-tier data |
| 26 | @system_plugin + @integral_read | Contradictory | Plugins receive external input; Tier 1 reads are internal |
| 27 | @fail_open + @deterministic | Suspicious | Fail-open with fallback defaults may produce non-deterministic output |
| 28 | @compensatable + @deterministic | Suspicious | Compensation introduces state changes that may affect determinism |
| 29 | @time_dependent + @idempotent | Suspicious | Time-dependent operations may not be idempotent across invocations |
- Python: Contradictory and suspicious decorator combinations (SCN-021)
Python additionally treats same-function @restoration_boundary composition with other transition decorators as an implementation extension when that combination would blur the boundary contract. In particular, @integral_read + @restoration_boundary and @integral_construction + @restoration_boundary remain contradictory in the reference scanner, so PY-WL-010 is satisfied only by an upstream traced restoration boundary on the serialisation path, not by same-function stacking.
Severity matrix. The Python binding inherits the parent specification's 8×8 severity matrix (Part I §8.3). Where the binding splits a framework rule into binding-specific sub-rules (e.g., WL-001 → PY-WL-001 and PY-WL-002), the sub-rules inherit the framework rule's severity matrix entries with the following binding-level deviation:
PY-WL-002 (attribute access with fallback default). PY-WL-002 derives from WL-001 but covers getattr(obj, name, default) and obj.attr or default. The obj.attr or default form has a falsy-substitution risk absent from dict-key access: it silently replaces present but falsy attribute values (0, "", False, None) with the default, not just missing attributes. This language-specific semantic risk — absent from the framework-level WL-001 pattern — justifies PY-WL-002 establishing its own matrix row under §8.1's split-rule provision. PY-WL-002 uses WARNING/RELAXED at EXTERNAL_RAW and UNKNOWN_RAW, and WARNING/STANDARD at MIXED_RAW (in all three cases departing from the framework's SUPPRESS/TRANSPARENT) because the falsy-substitution risk warrants visibility even at T4 boundaries where dict-key fallback defaults are expected and safe. This is a widening relative to the framework's WL-001 SUPPRESS at those cells, authorized by §8.1 for split sub-rules where language-specific semantics create risks absent from the framework pattern. See ADR-003 for the decision record.
The Python binding matrix for PY-WL-001 through PY-WL-010 (80 cells) is:
| Rule | Pattern | Integral | Assured | Guarded | Ext. Raw | Unk. Raw | Unk. Guarded | Unk. Assured | Mixed Raw |
|---|---|---|---|---|---|---|---|---|---|
| PY-WL-001 | Dict key access with fallback default | E/U | E/St | W/R | Su/T | Su/T | W/R | E/St | Su/T |
| PY-WL-002 | Attribute access with fallback default | E/U | E/St | W/R | W/R | W/R | W/R | E/St | W/St |
| PY-WL-003 | Existence-checking as structural gate | E/U | E/U | E/St | Su/T | Su/T | E/St | E/St | Su/T |
| PY-WL-004 | Catching all exceptions broadly | E/U | E/St | W/St | W/R | E/St | W/St | W/St | E/St |
| PY-WL-005 | Catching exceptions silently | E/U | E/St | W/St | W/R | E/St | W/St | W/St | E/St |
| PY-WL-006 | Integrity-critical writes in broad handlers | E/U | E/U | E/St | E/St | E/St | E/St | E/St | E/St |
| PY-WL-007 | Runtime type-checking internal data | E/St | W/R | W/R | Su/T | Su/T | W/R | W/R | W/St |
| PY-WL-008 | Validation with no rejection path | E/U | E/U | E/U | E/U | E/U | E/U | E/U | E/U |
| PY-WL-009 | Semantic validation without shape validation | E/U | E/U | E/U | E/U | E/U | E/U | E/U | E/U |
| PY-WL-010 | Tier 1 promotion without restoration evidence | E/U | E/U | E/U | E/U | E/U | E/U | E/U | E/U |
- Python: Severity matrix for binding rules PY-WL-001 through PY-WL-010
A.4.4 PY-WL-010: Tier 1 promotion on serialisation path without restoration evidence¶
PY-WL-010 maps to framework rule WL-009 (restoration symmetry). It is a structural verification rule. PY-WL-010 fires when all three conditions hold:
- A function is declared
@integral_reador@integral_construction(Group 1). - The function's data source is a manifest-declared serialisation boundary (identified via
BoundaryEntryobjects in the overlay'sboundariesarray withserialization_boundary: true). - The function's inputs do not trace through a declared restoration boundary with sufficient evidence within the two-hop analysis scope.
Python contract: same-function @integral_read + @restoration_boundary and @integral_construction + @restoration_boundary remain contradictory under SCN-021. In this binding, PY-WL-010 is satisfied only by an upstream declared restoration boundary on the serialisation path.
Analysis Level 1 minimum scope: direct source to a serialisation-marked restoration boundary, plus one undecorated intermediary hop. Level 3 may add transitive interprocedural recovery beyond that minimum, but it does not change the level-1 conformance claim for PY-WL-010.
Severity: ERROR/UNCONDITIONAL across all eight taint states (framework invariant, same as PY-WL-008 and PY-WL-009).
A.4.5 Supplementary rules: SUP-010 and SUP-011¶
SUP-010 and SUP-011 are binding-specific supplementary rules with no framework counterpart. They implement the non-normative deep-immutability principle from §9 of the prime spec. Both are opt-in supplementary enforcement and are not required for framework conformance.
SUP-010: Frozen dataclass with mutable container fields and no deep-freeze. A frozen dataclass (@dataclass(frozen=True)) with one or more fields whose type annotations indicate mutable containers (dict, list, set, Mapping, MutableMapping, or parameterised variants), where the __post_init__ method does not call a recognised deep-freeze function on those fields. The mutable container type list is extensible via mutable_container_types in scanner configuration.
Detection: AST visitor that identifies frozen dataclasses, inspects field annotations for mutable container types, and checks whether __post_init__ calls a recognised deep-freeze function (from deep_freeze_functions in scanner configuration) with self.<field> as argument.
SUP-011: Conditional freeze guard in __post_init__. An isinstance type guard in __post_init__ of a frozen dataclass that conditionally skips freezing based on the container type of a field. Matches isinstance calls where the first argument is self.<field>, any checked type is in {dict, tuple, MappingProxyType, frozenset, Mapping, list, set}, and the call is the test of an if whose body contains object.__setattr__ calls.
SUP-011 fires independently of SUP-010. Conditional freezing is independently problematic even when a deep-freeze call exists.
Severity matrix for SUP-010 and SUP-011:
| Rule | INTEGRAL | ASSURED | GUARDED | Ext. Raw | Unk. Raw | Unk. Guarded | Unk. Assured | Mixed Raw |
|---|---|---|---|---|---|---|---|---|
| SUP-010 | E/U | E/St | W/R | Su/T | Su/T | W/R | E/St | Su/T |
| SUP-011 | E/U | E/St | W/R | Su/T | Su/T | W/R | E/St | Su/T |
- Python: Severity matrix for supplementary rules SUP-010 and SUP-011
A.5 Type system and runtime enforcement¶
This section is non-normative.
Python uses three complementary enforcement mechanisms beyond the AST scanner:
Type system enforcement via typing.Annotated. Tier metadata is embedded in type hints: Annotated[str, Tier1, FailFast]. The Tier1, Tier2, Tier3, Tier4, and FailFast markers are annotation-only. They serve as documentation, scanner input (field-level tier classification), and mypy plugin integration (tier-flow checking). The mypy plugin's unique contribution is understanding Annotated[str, Tier1] as carrying tier metadata and flagging where a Tier4-annotated value flows to a Tier1-annotated parameter without validation.
Structural typing via typing.Protocol. Protocols encode trust requirements as structural types — e.g., ValidatedRecord requires _wardline_validated: bool and _wardline_trust_tier: Literal[1, 2]. Protocols use integer tier values (not string tokens) because Python's Literal types provide compile-time tier discrimination that mypy/pyright enforce statically. @runtime_checkable Protocols enable isinstance() checks as scaffolding for accidental tier mismatches, though they check structural conformance, not semantic authority.
Runtime structural enforcement via descriptors and __init_subclass__. AuthoritativeField descriptors raise on access-before-set, making fabricated defaults structurally impossible for Tier 1 fields. __init_subclass__ enforcement requires subclass methods to carry wardline decorators, firing at import time. Both are standard CPython OOP machinery shipped in the wardline-decorators package.
| Layer | When It Fires | What It Catches | Coverage |
|---|---|---|---|
| AST scanner | CI time | Pattern violations across entire codebase | Broad. Known false-negative surface (undecorated intermediaries, dynamic dispatch). |
| Type system (Protocols) | Development time (IDE) | Tier mismatches at function call sites | Narrow — only where mypy/pyright enabled. Optional. |
| Runtime (descriptors) | Access time | Fabricated defaults on Tier 1 fields | Narrow but absolute within scope. |
Runtime (__init_subclass__) | Import time | Unclassified methods on high-assurance base classes | Narrow but absolute within scope. |
- Python: Enforcement layer comparison by timing and coverage
A.6 Regime composition matrix¶
This section is non-normative.
The Python enforcement regime composes existing ecosystem tools with a reference implementation to achieve Wardline-Full conformance (Part I §15.4).
| Capability | Best Home | Profile | Rationale |
|---|---|---|---|
| Syntactic pattern detection (PY-WL-001 through PY-WL-005) | ruff rules | Advisory (not conformant) | Pure AST pattern match. Fast, fires at IDE time. Advisory only: no manifest, no tier-graded SARIF. |
| Tier-aware severity grading (all WL rules) | Reference scanner | Wardline-Core (authoritative) | Requires manifest consumption and decorator metadata for context-sensitive grading. |
| Taint-flow tracking between declared boundaries | Reference scanner | Wardline-Core | No existing tool consumes the manifest's trust topology. |
| Context-dependent rules (PY-WL-006 through PY-WL-010) | Reference scanner | Wardline-Core | Requires semantic context: audit-path awareness, tier classification, structural verification, validation ordering, restoration symmetry. |
| Type-layer tier-mismatch diagnostics | mypy plugin | Wardline-Type | Extends mypy's existing type-flow analysis with tier metadata. |
| Runtime tier enforcement | Decorator library | Foundation | Python-native OOP machinery; ships with decorator vocabulary. |
| Manifest validation, schema checking | wardline CLI | Wardline-Governance | Validates wardline.yaml, overlays, exception registers. |
| Fingerprint baseline management | wardline CLI | Wardline-Governance | Tracks annotation surface changes. |
| SARIF aggregation across regime tools | wardline CLI | Wardline-Governance | Combines per-tool SARIF into regime-level output. |
| Control-law state reporting | wardline CLI | Wardline-Governance | Reports normal/alternate/direct based on tool success. |
- Python: Regime composition matrix with tool assignments
The regime is temporally layered: ruff catches patterns while the developer types (advisory); the reference scanner grades them with tier-aware severity at CI time (authoritative). These are not redundant — they are layered by speed and precision.
Anti-recommendations. Do not force tier-aware taint analysis into ruff (its architecture is per-file, per-rule). Do not make mypy own governance artefacts. Do not use Semgrep as the normative rule source unless it can consume the wardline manifest faithfully. Do not build a pyright plugin until the mypy plugin is proven.
Stable interoperability surfaces. Third-party tools target: (1) manifest schema, (2) decorator metadata conventions, (3) rule identifiers and semantics, (4) golden corpus format, (5) SARIF property bags, (6) conformance profile vocabulary, (7) regime composition contract. If these interfaces are stable, any tool author can build wardline-compatible tooling without coordination with the specification owner.
A.7 Residual risks¶
This section is non-normative. Assessors evaluating a Python wardline deployment should review these risks alongside the framework-level residual risks (Part I §13), particularly risk 12 (evasion surface trajectory) — as annotation coverage grows, coding-level risk falls but governance risk rises, and the risks below should be read in that context.
A.7.1 Dynamic dispatch and metaprogramming¶
Python's dynamic nature creates fundamental blind spots for static analysis. getattr(obj, method_name)(), **kwargs forwarding, decorator factories that modify function signatures, and metaclass-generated methods are all invisible to the AST scanner. The scanner tags these as UNKNOWN — conservative but noisy in heavily dynamic codebases. Projects with heavy dynamic dispatch should use the coverage metric to measure whether the UNKNOWN surface is acceptable.
A.7.2 Taint laundering via serialisation¶
The serialisation boundary creates a fundamental blind spot for trust-escalation paths (a Python-specific instantiation of ACF-T1, authority tier conflation). Tier 4 data written to a database by one code path and read back through an @int_data-decorated function or a @restoration_boundary function may be classified at a higher tier than its actual provenance warrants. This is a structural limitation of static analysis in any language — no analysis depth can cross the serialisation boundary.
The @restoration_boundary decorator partially mitigates this by requiring declared provenance evidence categories and verifying that the function body contains corresponding checks. However, the correctness of the evidence remains governance-verified. @int_data without @restoration_boundary provides no evidence verification at all — the return value is tagged UNKNOWN_RAW.
Blast radius. The most cost-effective circumvention paths in the design are composed declarations — @int_data + @restoration_boundary with dishonest or incorrect evidence claims. The allow-list, deny-list, provenance justification, and fingerprint baseline are compensating controls.
A.7.3 Combined false-negative surface¶
Two analysis approximations compound at the first analysis level: the two-hop undecorated heuristic misses deeper indirection chains, and the function-level taint approximation treats all values within a function body under the enclosing function's trust classification. When both apply simultaneously, the scanner may miss tier-flow violations. The combined effect is multiplicative, not additive. Variable-level tracking (level 2) and transitive call-graph inference (level 3) eliminate these false-negative surfaces respectively.
A.7.4 Decorator selection correctness¶
The scanner verifies structural consistency given a declared decorator. It cannot verify that the decorator choice itself is correct. An agent that applies @fail_open to a function that should be @fail_closed produces no structural finding. Partial mitigations: contradictory-combination detection (SCN-021), suspicious-combination advisories, body-behaviour contradiction advisories, and baseline ratification with selection confirmation. The irreducible residual: choosing the correct decorator requires understanding business context — a semantic judgement the scanner cannot make.
A.7.5 Governance decay¶
The governance model specifies rigorous human gates: CODEOWNERS review, temporal separation, baseline ratification, provenance justification. Every one of these is a human activity. Under deadline pressure, each gate becomes a candidate for rubber-stamping. The scanner cannot verify the quality of the human judgement that governs the scanner's own trust topology. The governance capacity mechanisms defined in §10.4 — particularly the expedited governance ratio — provide quantitative signals that can detect governance decay before it reaches systemic rubber-stamping.
A.7.6 Fingerprint baseline deletion¶
Deleting wardline.fingerprint.json resets the entire governance history. If the scanner silently re-establishes the baseline, any injected misannotations become the accepted baseline with no diff. Compensating controls: the scanner distinguishes initial establishment from deletion by checking VCS history; CODEOWNERS protection on governance artefacts.
A.7.7 Runtime structural enforcement bypass¶
The AuthoritativeField descriptor stores values as obj.__dict__["_authoritative_{name}"]. Direct __dict__ manipulation bypasses the descriptor's __set__ sentinel check — a fundamental limitation of Python's descriptor protocol. Related reflective mechanisms (setattr(), vars(), and object.__setattr__()) create the same class of bypass when used against protected objects or fields, because they can route around the intended access path or mutate backing state without the descriptor's guard semantics. Compensating controls: AST scanner rules, fingerprint baseline, and supplementary reflective-write advisory findings (__dict__, setattr, vars, object.__setattr__) on authoritative data types.
A.7.8 Third-party library taint accuracy¶
Python applications commonly depend on third-party libraries for data processing, validation, and serialisation — Pydantic, marshmallow, pandas, requests, and similar packages. These libraries execute in-process but are outside the wardline's annotation surface. The framework's dependency_taint declarations (§14.1.2) allow the overlay to assign taint states to third-party function return values, but the accuracy of those declarations depends on governance review, not machine verification.
Two Python-specific concerns sharpen this risk. First, Pydantic model defaults on fields that participate in tier-classified data flows are subject to the Group 5 scanning requirement (§7, SHOULD). When a Pydantic model is defined in a third-party library, the enforcement tool's ability to scan those defaults depends on whether it analyses installed package source — which is binding-specific and not specified in the §A.3 interface contract. Library-defined Pydantic defaults that escape scanning create a gap at exactly the point where §7 Group 5 says they SHOULD be caught. Second, the two-hop call-graph heuristic (§9.1) that enables WL-007 delegation analysis may or may not follow calls into third-party library source depending on the scanner's resolution of installed packages. A @validates_shape function whose body delegates to a library function (e.g., return my_library.validate(raw)) satisfies WL-007 only if the scanner follows the delegation and finds a rejection path in the library's source. If the scanner does not resolve library internals, the delegation appears to have no rejection path.
Compensating controls: dependency_taint declarations with version pinning; the application's own validation boundaries as the terminal control; governance review of taint declarations when dependency versions change; the two-hop heuristic as a best-effort mechanism for delegation resolution into available source.
A.8 Worked example with SARIF output¶
This section is non-normative. It demonstrates decorators in context through the full tier lifecycle — from raw external input to authoritative artefact — proving implementability.
Scenario. A risk assessment system receives partner data from an external API, validates it, and produces an authoritative risk assessment record for the audit trail.
Data flow:
External API response (T4)
→ parse_partner_response() → PartnerDTO (T3)
→ validate_partner_semantics() → ValidatedPartner (T2)
→ create_risk_assessment() → RiskAssessment (T1)
Step 1: External boundary — receiving raw data (T4)
from wardline import external_boundary
@external_boundary
def fetch_partner_data(partner_id: str) -> dict:
"""Returns T4 raw data from external partner API."""
response = requests.get(f"{PARTNER_API_URL}/{partner_id}")
response.raise_for_status()
return response.json()
Return value tagged EXTERNAL_RAW. No field access, no defaults, no assumptions about structure.
Step 2: Shape validation — establishing structure (T4 → T3)
from dataclasses import dataclass
from wardline import validates_shape, schema_default
@dataclass(frozen=True)
class PartnerDTO:
"""T3 — shape-validated. Safe to handle; values unchecked."""
partner_id: str
name: str
country_code: str
classification: str
risk_indicators: list[str]
@validates_shape
def parse_partner_response(raw: dict) -> PartnerDTO:
"""T4 → T3. Establishes structural guarantee."""
required = {"partner_id", "name", "country_code", "security_classification"}
missing = required - raw.keys()
if missing:
raise SchemaError(f"Missing fields: {missing}")
for field in ("partner_id", "name", "country_code", "security_classification"):
if not isinstance(raw[field], str):
raise SchemaError(
f"{field}: expected str, got {type(raw[field]).__name__}"
)
indicators = schema_default(raw.get("risk_indicators", []))
if not isinstance(indicators, list):
raise SchemaError(
f"risk_indicators: expected list, got {type(indicators).__name__}"
)
for i in indicators:
if not isinstance(i, str):
raise SchemaError(
f"risk_indicators item: expected str, got {type(i).__name__}"
)
return PartnerDTO(
partner_id=raw["partner_id"],
name=raw["name"],
country_code=raw["country_code"],
classification=raw["security_classification"],
risk_indicators=indicators,
)
Note: risk_indicators is optional-by-contract — the external API may omit it. The schema_default() wrapper links this .get() to the overlay declaration for this data source, which declares the field as optional with an approved default of []. Without schema_default(), the .get() would fire PY-WL-001 at ERROR/STANDARD severity.
Step 3: Semantic validation — establishing domain fitness (T3 → T2)
from wardline import validates_semantic
@dataclass(frozen=True)
class ValidatedPartner:
"""T2 — semantically validated for landscape recording and reporting."""
partner_id: str
name: str
country_code: str
classification: str
risk_indicators: tuple[str, ...]
# validation_scope declared in overlay with contracts:
# "landscape_recording", "partner_reporting"
@validates_semantic
def validate_partner_semantics(dto: PartnerDTO) -> ValidatedPartner:
"""T3 → T2. Domain fitness for landscape and reporting consumers."""
if dto.country_code not in VALID_COUNTRY_CODES:
raise DomainValidationError(
f"Unrecognised country code: {dto.country_code!r}"
)
if dto.classification not in VALID_CLASSIFICATION_LEVELS:
raise DomainValidationError(
f"Invalid classification: {dto.classification!r}"
)
if not dto.name.strip():
raise DomainValidationError("Partner name is empty")
if len(dto.name) > MAX_PARTNER_NAME_LENGTH:
raise DomainValidationError(
f"Name exceeds {MAX_PARTNER_NAME_LENGTH} characters"
)
for indicator in dto.risk_indicators:
if indicator not in KNOWN_RISK_INDICATORS:
raise DomainValidationError(
f"Unknown risk indicator: {indicator!r}"
)
return ValidatedPartner(
partner_id=dto.partner_id,
name=dto.name.strip(),
country_code=dto.country_code,
classification=dto.classification,
risk_indicators=tuple(dto.risk_indicators),
)
Step 4: Trusted construction — creating institutional authority (T2 → T1)
from wardline import integral_construction
@integral_construction
def create_risk_assessment(
partner: ValidatedPartner,
context: AuditContext,
) -> RiskAssessment:
"""T2 → T1. Produces an authoritative risk assessment.
This is an institutional act, not a data transformation."""
return RiskAssessment(
assessment_id=generate_assessment_id(),
partner_id=partner.partner_id,
partner_name=partner.name,
risk_level=compute_risk_level(partner),
classification=partner.classification,
assessed_by=context.identity,
assessed_at=context.timestamp,
)
The complete call chain:
def assess_partner(partner_id: str, context: AuditContext) -> RiskAssessment:
"""Full pipeline: T4 → T3 → T2 → T1."""
raw = fetch_partner_data(partner_id) # T4
dto = parse_partner_response(raw) # T3
validated = validate_partner_semantics(dto) # T2
assessment = create_risk_assessment( # T1
validated, context
)
return assessment
Each line is a tier transition. Each function has one decorator declaring one transition. A reviewer can read this pipeline and trace the tier at every step.
Corresponding SARIF output. If the monolith version of this code were scanned — e.g., a function that uses raw_data.get("security_classification", "OFFICIAL") in a Tier 1 context — the scanner would produce:
{
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "wardline-scanner",
"version": "0.2.0",
"rules": [{
"id": "PY-WL-001",
"shortDescription": {
"text": "Dictionary key access with fallback default"
},
"defaultConfiguration": { "level": "error" }
}]
}
},
"results": [{
"ruleId": "PY-WL-001",
"level": "error",
"message": {
"text": "Fabricated default on tier-sensitive path: .get(\"security_classification\", \"OFFICIAL\")"
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": "src/adapters/partner_adapter.py"
},
"region": {
"startLine": 42,
"snippet": {
"text": "raw_data.get(\"security_classification\", \"OFFICIAL\")"
}
}
},
"logicalLocations": [{
"fullyQualifiedName": "myproject.adapters.partner_adapter.process_partner_update",
"kind": "function"
}]
}],
"properties": {
"wardline.rule": "PY-WL-001",
"wardline.taintState": "INTEGRAL",
"wardline.severity": "ERROR",
"wardline.exceptionability": "UNCONDITIONAL",
"wardline.analysisLevel": 1,
"wardline.enclosingTier": 1,
"wardline.annotationGroups": [1],
"wardline.excepted": false,
"wardline.dataSource": "partner-api"
}
}],
"properties": {
"wardline.manifestHash": "sha256:a1b2c3d4e5f6...",
"wardline.coverageRatio": 0.73,
"wardline.controlLaw": "normal",
"wardline.deterministic": true
}
}]
}
The SARIF output carries: the binding rule ID (PY-WL-001), the taint state of the enclosing context (INTEGRAL), the tier-graded severity (ERROR), the exceptionability class (UNCONDITIONAL — this finding cannot be excepted), and the analysis level. An assessor reading this finding knows immediately: a fabricated default was detected in a Tier 1 (audit trail) context, it is an unconditional error, and no exception can suppress it.
Agent guidance note. When generating code that interacts with wardline-annotated boundaries, agents should determine the input tier, the expected output tier, and the required transition — then apply the most specific decorator. If the tier is unknown, leave the function unannotated; UNKNOWN is safer than a wrong declaration. Full agent guidance is maintained as a living document outside the specification (evolved from Part III §37).
Annotation change impact preview. Python binding implementations SHOULD support annotation change impact preview using the SARIF metadata defined in Part I §11.1. When a developer modifies a tier assignment or decorator — e.g., changing @validates_shape to @validates_external, or promoting a module from Tier 3 to Tier 2 — the tool shows the cascade: newly applicable pattern rules, resolved findings, severity changes, and affected modules. The primary span is the changed annotation; secondary spans (carried in SARIF relatedLocations) are code locations whose compliance status changes. This gives developers and reviewers a before-and-after view of a governance change before it is committed, reducing the risk of annotation changes that inadvertently widen the enforcement surface or silently resolve findings that should remain visible.
A.9 Adoption strategy¶
This section is non-normative.
Adoption follows a phased model. Each phase is independently valuable.
| Phase | Components | Coverage |
|---|---|---|
| 1: Decorators + advisory ruff rules | wardline-decorators, wardline-ruff | IDE-time and pre-commit advisory warnings for PY-WL-001 through PY-WL-005 at uniform severity. No manifest required. Lowest-cost entry point. |
| 2: Manifest + reference scanner | Add wardline.yaml, wardline-scanner | Tier-aware severity grading for all ten binding rules. Taint-flow tracking. SARIF output. Governance-grade findings begin here. |
| 3: Type-system enforcement | wardline-mypy | Development-time tier-mismatch diagnostics. Requires typing.Annotated tier annotations on data models. |
| 4: Runtime structural enforcement | AuthoritativeField descriptors, __init_subclass__ bases | Specific high-risk paths become structurally impossible to violate. Deploy on audit records and decision products first. |
| 5: Full regime governance | wardline-cli | Fingerprint baseline tracking, exception register management, SARIF aggregation, control-law state reporting. Provides governance evidence for independent assessment. |
- Python: Adoption phases with components and coverage
Annotation budget. Target ~50–100 decorators for an 80k-line codebase in the initial annotation pass — external boundaries (15–25), validators (15–25), audit writers/readers (10–15), sensitive data handlers (5–10), layer declarations (module-level). Phases are a recommended ordering, not a mandatory sequence; a project may skip Phase 3 (no mypy) and still achieve Phases 1, 2, 4, and 5.
A.10 Error handling and control law¶
This section is non-normative.
Scanner error handling.
| Scenario | Behaviour |
|---|---|
| Syntax error in Python file | Skip; WARNING. Escalate to ERROR if file is in Tier 1 module. |
| Unresolvable import | Tag UNKNOWN in symbol table. |
| Unrecognised decorator | Ignore. |
Invalid wardline.yaml | Exit non-zero. Do not scan. |
Missing wardline.yaml | Exit non-zero. Do not scan. |
Missing wardline.toml | Run with defaults (all groups enabled). Advisory. |
| Baseline file missing (initial) | Record current surface as baseline. |
| Baseline file missing (deleted) | ERROR, exit non-zero. Do not silently re-establish. |
| File exceeds size limit | Skip; WARNING. Escalate to ERROR if Tier 1 module. |
- Python: Scanner error handling behaviour by scenario
Exit codes: 0 (no ERROR findings), 1 (at least one ERROR finding), 2 (internal error), 3 (direct law — regime cannot produce meaningful enforcement output; wardline regime only).
Regime-level control-law state transitions.
| Scenario | Control Law | Impact |
|---|---|---|
| All configured tools ran successfully | Normal | Full enforcement |
| ruff plugin unavailable or failed | Alternate | Advisory fast-path absent; reference scanner provides authoritative coverage at CI time |
| mypy plugin unavailable or failed | Alternate | Type-layer diagnostics absent; no compensating tool at development time |
| Reference scanner unavailable | Alternate (severe) | Authoritative analysis absent; ruff provides advisory coverage for five of ten rules only |
| Manifest validation failed | Direct | Trust topology unavailable; no governance-grade findings possible |
| wardline CLI itself unavailable | Direct | No regime orchestration, SARIF aggregation, or control-law reporting |
- Python: Control law state transitions by scenario
The distinction between alternate and direct law follows Part I §10.5: alternate means degraded but running; direct means no meaningful enforcement output. Changes to wardline policy artefacts MUST NOT proceed under direct-law bypass.
A.11 Conformance criteria mapping¶
This section is non-normative. It maps the ten conformance criteria from Part I §15 to the Python binding's implementation artefacts.
| # | Criterion (§15) | Implementation | Evidence / CLI |
|---|---|---|---|
| 1 | Annotation vocabulary covers 17 groups | wardline-decorators package: 16/17 groups enforced; Group 16 @data_flow declared advisory-only | §A.4.2 decorator table; wardline.toml group configuration |
| 2 | Pattern rules WL-001–WL-006 detected | PY-WL-001 through PY-WL-007 (WL-001 splits into two) | wardline scan; SARIF implementedRules; corpus specimens |
| 3 | Structural verification WL-007/WL-008/WL-009 | PY-WL-008 (rejection path), PY-WL-009 (validation ordering), PY-WL-010 (restoration symmetry) | wardline corpus verify; engine L3 integration tests |
| 4 | Taint-flow tracking (direct + two-hop) | Three-level taint: L1 function, L2 variable, L3 callgraph with SCC fixed-point | wardline scan; wardline.analysisLevel in SARIF |
| 5 | Precision/recall measured per cell | wardline corpus publish computes TP/FP/TN/FN per (rule × taint) cell for the binding rule matrix | wardline corpus verify --json; per-cell report over the binding matrix |
| 6 | Golden corpus maintained | corpus_manifest.json indexes the live specimen set, including adversarial and suppression-interaction categories for the implemented binding surface | corpus/; corpus_manifest.json; wardline corpus verify |
| 7 | Self-hosting gate | test_self_hosting_scan.py runs scanner against src/wardline/; per-rule baseline regression | uv run pytest -k self_hosting; CI self-hosting-scan job |
| 8 | Deterministic SARIF v2.1.0 with property bags | sarif.py emits v2.1.0; --verification-mode for byte-identical output | wardline scan --verification-mode; test_determinism.py |
| 9 | Governance model (protected files, temporal separation, fingerprint) | CODEOWNERS protects all governance artefacts; wardline.yaml declares temporal_separation; wardline fingerprint provides baseline tracking | wardline fingerprint diff; wardline regime status |
| 10 | Manifest consumed and schema-validated | loader.py validates against JSON Schema before producing findings; exit code 2 on invalid manifest | wardline manifest validate; wardline scan (validates first) |
- Python: Conformance criteria mapping to implementation artefacts
Regime composition. The following table maps each criterion to the enforcement tool(s) that address it:
| Criterion | wardline-scanner | wardline-ruff (advisory) | wardline-mypy (optional) | wardline-cli |
|---|---|---|---|---|
| 1 (vocabulary) | Discovery + enforcement | Partial (5 rules) | Type annotations | — |
| 2 (pattern rules) | All 10 rules | 5 of 10 (advisory) | — | — |
| 3 (structural verification) | PY-WL-008, PY-WL-009, PY-WL-010 | — | — | — |
| 4 (taint tracking) | L1–L3 | — | — | — |
| 5 (precision/recall) | — | — | — | corpus publish |
| 6 (corpus) | — | — | — | corpus verify |
| 7 (self-hosting) | Self-scan in CI | — | — | — |
| 8 (SARIF output) | Producer | — | — | Consumer/aggregator |
| 9 (governance) | — | — | — | All governance commands |
| 10 (manifest) | Consumer | — | — | manifest validate |
- Python: Conformance criteria coverage by enforcement tool
A.12 Branch protection and CI gating¶
This section is non-normative. It documents adopter responsibilities for CI enforcement.
The reference scanner runs as a CI job (self-hosting-scan in ci.yml). However, configuring this job as a required status check is an adopter-side GitHub (or equivalent) setting — not something the tool itself can enforce.
Adopters SHOULD:
- Enable the
self-hosting-scanworkflow as a required status check on the default branch. - Configure branch protection rules to require the scan to pass before merging.
- Add
wardline.yaml, overlay files, the exception register, and fingerprint baselines to CODEOWNERS.
The wardline toolchain provides the enforcement mechanisms; the adopter provides the branch protection configuration that gates merges on those mechanisms.
A.13 Provenance justification guidelines¶
This section is non-normative. It documents expected provenance fields for trust escalation.
When declaring trust escalation in wardline.yaml boundary entries, the provenance field accepts a freeform dictionary. For assessor clarity, the following fields are RECOMMENDED:
| Field | Type | Purpose |
|---|---|---|
rationale | string | Why this escalation is justified (1–2 sentences) |
evidence_type | string | One of: structural, semantic, institutional, restoration |
reviewer | string | Identity of the person who approved the escalation |
date | string (ISO 8601) | Date the escalation was approved |
ticket | string | Link to the review ticket or PR |
- Python: Recommended provenance justification fields for trust escalation
Example:
boundaries:
- function: "validate_partner_record"
transition: "TRUST_ELEVATION"
from_tier: 4
to_tier: 2
provenance:
rationale: "Combined shape + semantic validation with rejection path"
evidence_type: "structural"
reviewer: "John"
date: "2026-03-27"
ticket: "wardline#142"
The scanner checks that provenance is present for escalations but does not enforce field completeness. Assessors evaluating governance quality SHOULD verify that provenance entries contain sufficient rationale for the claimed escalation.
A.14 Governance profile graduation guide¶
This section is non-normative. It documents the path from Lite to Assurance governance profile.
Prerequisites for Assurance. A deployment currently at governance_profile: "lite" can graduate to Assurance when all five conditions are met:
| # | Condition | How to verify |
|---|---|---|
| 1 | Golden corpus expanded to 126+ specimens | wardline corpus verify --json → total_specimens >= 126 |
| 2 | Fingerprint baseline established with at least one review cycle | wardline fingerprint diff shows a prior baseline |
| 3 | Temporal separation operational (different-actor review, no alternatives) | Remove temporal_separation.alternative from wardline.yaml |
| 4 | Expedited ratio threshold declared | Add metadata.expedited_ratio_threshold to wardline.yaml as a proportion in [0,1] (for example 0.15) |
| 5 | All Lite-era exceptions reviewed under Assurance governance | wardline exception list --needs-review returns empty |
- Python: Prerequisites for Assurance governance profile graduation
Steps:
- Expand the corpus to meet the 126-specimen floor, with per-rule AFP and AFN specimens.
- Run
wardline fingerprint updateto establish the baseline. Complete one full review cycle. - Configure temporal separation: ensure all governance artefact changes are reviewed by a different actor. Remove the
same-actor-with-retrospectivealternative from the manifest. - Add
metadata.expedited_ratio_thresholdto the manifest metadata section as a ratio value in[0,1](for example0.15for 15%). - Review all existing exceptions: confirm reviewer identity, validate rationale against current context, update expiry dates.
- Change
governance_profile: "lite"togovernance_profile: "assurance"inwardline.yaml. - Run
wardline coherenceto validate the new profile.
A.15 Dependency taint (§6.5)¶
This section is non-normative. It documents the Python binding's approach to third-party dependency taint.
Manifest declaration. Third-party library function return taints are declared in the root manifest under dependency_taint:
dependency_taint:
- package: "requests>=2.0,<3.0"
function: "requests.get"
returns_taint: "EXTERNAL_RAW"
rationale: "HTTP response is untrusted external data"
- package: "requests>=2.0,<3.0"
function: "requests.post"
returns_taint: "EXTERNAL_RAW"
rationale: "HTTP response is untrusted external data"
Each entry has four required fields: package (identifier with optional version constraint), function (fully-qualified name), returns_taint (one of the eight canonical taint states), and rationale (governance justification for the declaration).
Resolution. At scan time, the engine resolves declarations against each file's import statements:
| Import Form | Call Syntax | Resolution |
|---|---|---|
import requests | requests.get(url) | requests.get → FQN match → declared taint |
import requests as req | req.get(url) | req → alias for requests → requests.get → declared taint |
from requests import get | get(url) | get → FQN requests.get → declared taint |
- Python: Dependency taint resolution by import form
UNKNOWN_RAW fallback (§6.5 MUST). When a call targets a function in a package that has dependency_taint declarations, but the specific function is not declared, the return taint is UNKNOWN_RAW. This prevents undeclared library functions from inheriting the caller's taint — a conservative default that surfaces as a finding if the return value reaches a high-tier code path.
Compound patterns. The following compound patterns fall back to UNKNOWN_RAW in v1.0:
| Pattern | Example | v1.0 Behaviour |
|---|---|---|
| Method chaining | df.groupby("x").agg({"y": "sum"}) | UNKNOWN_RAW (intermediate not tracked) |
| Generator iteration | for row in cursor.fetchall() | Inherits iterable taint via for target |
| Context managers | with db.connect() as conn | Inherits context expr taint via with target |
| Async variants | async for item in stream() | Same as sync equivalents |
- Python: Compound pattern handling in v1.0 dependency taint
Method chaining beyond the first call is not resolved. Declare the root function; downstream method results that are assigned to variables inherit through the variable taint system.
Dependency taint is not a boundary declaration. The manifest declares what data is when it arrives from ungoverned code. The application's own annotated boundaries declare what happens to it next.