Skip to content
Disclaimer

This is a draft discussion paper circulated for peer review. It is independent work published in a personal capacity and does not constitute official guidance or policy of any government body. It does not mandate or recommend specific controls for any agency, system, or project. Views and analysis are the author's own.

Part II-A: Python Language Binding Reference

This section provides the Python-specific binding reference for the Wardline framework specification. 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.

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 a governing body owns the specification, not the code.


A.2 Python language evaluation

This section is non-normative. It models how future binding authors should assess their language against the framework specification'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.
Tooling ecosystem Strong mypy, pyright, ruff, bandit, and extensive AST tooling. SARIF is the standard interchange format.

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

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:

  1. Manifest consumption. The tool MUST consume the wardline manifest (wardline.yaml and 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.yaml is the trust topology manifest. wardline.toml is the scanner's operational configuration. The two files serve different purposes.)

  2. 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 canonical decorator names and their argument schemas are defined in A.4.

  3. Schema default recognition. The tool MUST recognise schema_default() as a PY-WL-001 suppression marker. Calls wrapped in schema_default() where the default value matches the overlay's declared approved default are governed by the overlay declaration, not by PY-WL-001.

  4. SARIF output. The tool MUST produce findings in SARIF v2.1.0 with the wardline-specific property bags defined in the parent specification. The Python regime requires the following mandatory property bag keys on each result object:

    Key Type Description
    wardline.rule string Binding rule ID (e.g., PY-WL-001)
    wardline.taintState string Canonical taint state token (e.g., AUDIT_TRAIL)
    wardline.enclosingTier integer Tier of the enclosing context (1, 2, 3, or 4)
    wardline.severity string ERROR, WARNING, or SUPPRESS
    wardline.exceptionability string UNCONDITIONAL, STANDARD, RELAXED, or TRANSPARENT
    wardline.analysisLevel integer Analysis level that produced the finding (1, 2, or 3)
    wardline.excepted boolean Whether an active exception covers this finding
    wardline.annotationGroups integer array Annotation groups active on the enclosing function
  5. Rule declaration. The tool MUST declare which rules it implements and MUST maintain golden corpus specimens for those rules.

  6. Verification mode. The tool SHOULD support the --verification-mode output profile for deterministic byte-identical output against the golden corpus.

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 nine binding rules (PY-WL-001 through PY-WL-009) with tier-aware severity grading is a complete Wardline-Core tool.

Rule mapping. The nine Python binding rules derive from the eight framework rules (Part I) 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, hasattr() guards)
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-001 through PY-WL-005 are syntactic patterns detectable by per-file AST analysis (the ruff advisory path). PY-WL-006 through PY-WL-009 require semantic context — audit-path awareness, tier classification, structural verification, or validation ordering — 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 implies distinct programming styles:

Tier Posture Philosophy
Tier 4 Sceptical programming Treat everything as hostile sludge. Validate structure first, normalise, reject.
Tier 3 Guarded programming Structure is trustworthy. Direct field access is safe; validate domain constraints before using values in business logic.
Tier 2 Confident programming Structure and domain meaning are trustworthy within the declared bounded context. Guard only cross-cutting concerns.
Tier 1 Offensive programming (assert invariants; never silently recover) Assume invariants, detonate on breach. Anomalies must surface immediately as faults.

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 the framework specification. This table provides the Python-specific decorator syntax. Decorators set _wardline_* metadata attributes on the decorated callable; they do almost nothing at runtime.

# 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) Body must contain rejection path (WL-007). T4 → T3 constructor.
1 @validates_semantic (none) Body must contain rejection path (WL-007). Inputs must trace to @validates_shape output (WL-008/PY-WL-009). T3 → T2 constructor. Bounded context declared in overlay.
1 @validates_external (none) Combined T4 → T2. Body must contain rejection path (WL-007). Must perform both structural and semantic checks.
1 @tier1_read (none) Body bans: .get() with defaults, getattr() with fallbacks, hasattr(), broad except. Return tagged TIER_1.
1 @audit_writer (none) Call-site bans: enclosing swallowing except. Audit must dominate telemetry on shared execution paths. Fallback paths that bypass the audit call produce a finding. Return tagged TIER_1.
1 @authoritative_construction (none) Same body restrictions as @tier1_read. Semantically equivalent to @audit_writer but for non-audit authoritative artefacts. Return tagged TIER_1.
2 Audit Primacy @audit_critical (none) Superset of @audit_writer — call sites must not have fallback paths that skip the audit call.
3 Plugin/Component 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) AUDIT_TRAIL 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 must 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 must be in __init__, __post_init__, or setup methods. 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 must be preceded by existence/dedup guard.
9 @atomic (none) Multiple state-modifying calls must be within transaction context.
9 @compensatable(rollback=fn) rollback: reference to rollback function Rollback function must exist with compatible signature.
10 Failure Mode @fail_closed (none) Same body restrictions as @tier1_read. Carries implicit @must_propagate. Severity lookups use AUDIT_TRAIL.
10 @fail_open (none) Explicitly permits graceful degradation patterns. Composition requirement: must carry a trust classification decorator (WARNING if alone).
10 @emits_or_explains (none) Every return/exit path must reach an emit call or an explain/logging call.
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 must propagate to an @exception_boundary. No intermediate catch-and-continue.
10 @preserve_cause (none) Every raise X(...) in except blocks must include from clause. One-hop traversal into helper calls.
11 Data Sensitivity @handles_pii(fields=[...]) fields: list of PII field names Named fields must not reach logger, error messages, unprotected persistence.
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 Body must contain rejection path. 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 must protect shared mutable state or be pure.
13 @ordered_after(name) name: function name that must precede At call sites where both functions appear, named function must lexically precede.
13 @not_reentrant (none) Call graph cycle detection through the decorated function.
14 Access/Attribution @requires_identity (none) Identity-typed parameter must appear in @audit_writer/@audit_critical call within body.
14 @privileged_operation (none) Authorisation check must precede state-modifying call.
15 Lifecycle/Scope @test_only (none) No production module may import this symbol.
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 must satisfy WL-007. Evidence must support claimed tier per the evidence matrix in the framework specification. Scanner demotes effective taint state when evidence is insufficient.

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)
@tier1_read Sources TIER_1 (no from_tier)
@audit_writer @trust_boundary(from_tier=2, to_tier=1) + call-site enforcement Audit-ordering semantics not expressible via @trust_boundary alone
@authoritative_construction @trust_boundary(from_tier=2, to_tier=1)

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.

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. 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 must establish that the body performs both structural and semantic checks. 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 SHAPE_VALIDATED No — structural guarantees already established
@validates_external TIER_4 EXTERNAL_RAW Yes — encompasses shape validation

@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 AUDIT_TRAIL-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 @audit_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 + @tier1_read Contradictory Tier 1 requires offensive programming — fail-open is structurally incompatible
3 @fail_open + @audit_writer Contradictory Audit writes must not silently degrade
4 @fail_open + @authoritative_construction Contradictory Authoritative artefacts must not have fallback construction paths
5 @fail_open + @audit_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 + @tier1_read Contradictory External data is Tier 4; Tier 1 reads are internal
8 @external_boundary + @authoritative_construction Contradictory External data cannot be directly authoritative
9 @validates_shape + @validates_semantic Contradictory Use @validates_external for combined T4→T2
10 @validates_shape + @tier1_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 @tier1_read + @restoration_boundary Contradictory Tier 1 reads access existing authoritative data; restoration reconstructs from raw representation
17 @audit_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 @audit_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 uses restoration boundaries, not the T4→T3 shape-validation mechanism designed for external input
23 @preserve_cause + @exception_boundary Contradictory (Alias of #12 — @preserve_cause implies propagation)
24 @compensatable + @audit_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 + @tier1_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

Severity matrix. The Python binding extends the parent specification's 8×8 severity matrix to 9×8 by splitting WL-001 into PY-WL-001 and PY-WL-002. No cell values are changed — the added PY-WL-002 row inherits WL-001's severity matrix entries with no additional SUPPRESS cells. The total is 26 UNCONDITIONAL cells (25 inherited from the framework matrix plus PY-WL-002's AUDIT_TRAIL entry). The Java binding (Part II-B) documents two cells where Java's type system structurally prevents certain violations, warranting SUPPRESS; Python's weaker type system does not support equivalent structural guarantees, so no cells are modified.


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.

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 per the framework specification.

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-009) Reference scanner Wardline-Core Requires semantic context: audit-path awareness, tier classification, structural verification, validation ordering.
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.

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's maintainers.


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 in the framework specification, 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 the framework specification — 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. Compensating controls: AST scanner rules, fingerprint baseline, and supplementary __dict__-access advisory findings.

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 (§13.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 (§6, 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 §6 Group 5 says they SHOULD be caught. Second, the two-hop call-graph heuristic (§8.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 government 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 contract."""
    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, ...]

# bounded_context declared in overlay: consumers:
#   ["record_to_landscape", "generate_partner_report"]
@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 authoritative_construction

@authoritative_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": "AUDIT_TRAIL",
        "wardline.severity": "ERROR",
        "wardline.exceptionability": "UNCONDITIONAL",
        "wardline.analysisLevel": 1,
        "wardline.enclosingTier": 1,
        "wardline.annotationGroups": [1],
        "wardline.excepted": false
      }
    }],
    "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 (AUDIT_TRAIL), 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.

Annotation change impact preview. Python binding implementations SHOULD support annotation change impact preview using the SARIF metadata defined in the framework specification. 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 nine 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.

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.

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 nine 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

The distinction between alternate and direct law follows the framework specification: alternate means degraded but running; direct means no meaningful enforcement output. Changes to wardline policy artefacts MUST NOT proceed under direct-law bypass.