Skip to content

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:

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

  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 (§11.1). 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., INTEGRAL)
    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)
    Python: Mandatory SARIF property bag keys for Wardline-Core result objects

    wardline.analysisLevel values 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 intermediaries
    • 2 — level 1 plus variable-level taint tracking within function bodies and container-sensitive propagation beyond whole-function approximation
    • 3 — 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: 1 for 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.

  1. Decorator composition. The tool MUST resolve decorator stacking on the same function. In particular, @int_data without @restoration_boundary produces UNKNOWN_RAW; @int_data composed with @restoration_boundary produces the effective tier determined by the Part I §6.3 evidence matrix.

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

  3. Rule declaration. The tool MUST declare which rules it implements and MUST maintain golden corpus specimens for those rules.

  4. Verification mode. The tool MUST support the --verification-mode output profile for deterministic byte-identical output against the golden corpus (Part I §11, property 5).

  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, and wardline.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:

  1. A function is declared @integral_read or @integral_construction (Group 1).
  2. The function's data source is a manifest-declared serialisation boundary (identified via BoundaryEntry objects in the overlay's boundaries array with serialization_boundary: true).
  3. 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:

  1. Enable the self-hosting-scan workflow as a required status check on the default branch.
  2. Configure branch protection rules to require the scan to pass before merging.
  3. 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 --jsontotal_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:

  1. Expand the corpus to meet the 126-specimen floor, with per-rule AFP and AFN specimens.
  2. Run wardline fingerprint update to establish the baseline. Complete one full review cycle.
  3. Configure temporal separation: ensure all governance artefact changes are reviewed by a different actor. Remove the same-actor-with-retrospective alternative from the manifest.
  4. Add metadata.expedited_ratio_threshold to the manifest metadata section as a ratio value in [0,1] (for example 0.15 for 15%).
  5. Review all existing exceptions: confirm reviewer identity, validate rationale against current context, update expiry dates.
  6. Change governance_profile: "lite" to governance_profile: "assurance" in wardline.yaml.
  7. Run wardline coherence to 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 requestsrequests.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.