Declare the layer per GH-DEC-2026-012; close T02

Gate House ruled all three questions within a day, attributing the speed to the
request being filed before the architecture with candidate answers and their
costs.

R1 PEP-shaped, confirmed as proposed. The ruling settles the shape; the layer
stays ours to declare, so layer.yaml is written in this repository's voice
rather than transcribed from the reply.

R2 yes to a presentation claim, no second catalog row, under three limits now
declared in layer.yaml and tested. Limit 2 — the claim must never be an input to
the decision it presents for — is load-bearing: our self-dealing argument was
accepted because it holds, not despite it. Limit 3 drives architecture, since
here the actor being audited and the evidence source are the same component.

R3 (b) with the authority rule: binding digest authoritative for what the
request is, view_hash only for what was shown, neither substitutable, and a
disagreement between them is a finding against the presenting surface rather
than a fact about the request. Linkage is co-reference; nesting was refused
because it reproduces the GH-DEC-2026-008 hash cycle.

Built to v0.8 obligation 3 rather than migrating later: axis enumerated, unknown
resolves to fail_closed, absent distinguishable from unknown in the record, and
published-equals-shipped asserted by test rather than claimed. Every stance is
fail_closed, which is a conclusion not a shortcut — ops-warden can justify
fail_open on a continuity argument that does not exist here.

GH-DEC-2026-010 inherited as a declared gap in four documents: a decision cannot
today be proven to have come from access-engine. The decision path must not be
described as validated while FLEX-WP-0024 is open.

46 tests pass. T05 and T07 unblocked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3W1dQG7GFFM9d94jFx7iR

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1565372@bnt-lap001
Assistant-Session: 16bb2f25-b34c-49ef-8e94-5fec3567a568
This commit is contained in:
tegwick 2026-09-09 22:25:35 +02:00
parent f36e2b789a
commit f6376ddff5
9 changed files with 614 additions and 20 deletions

24
GOAL.md
View file

@ -85,6 +85,11 @@ These hold for Stage 1 and for every stage after it.
- **`approved` is never rendered as permission to act.** It is a state of an
object. Presenting it as "you may now do the thing" is a policy decision point
in the browser.
- **The decision path is not validated, and must not be described as such.**
`GH-DEC-2026-010` requires a decision be attributable to `access-engine`, and
no consumer can satisfy that today — the envelope is unsigned
(`FLEX-WP-0024`). This surface records that a decision was obtained and what
it said, and records `decision_attributable: false`.
- **Entitlement to view is `access-engine`'s.** A `200` from `approval-engine`
is not permission to see the approval; that engine never answers "may this
actor do X". This surface obtains a decision before rendering and never
@ -108,7 +113,9 @@ These hold for Stage 1 and for every stage after it.
> **Who owns the approver UI?**
The answer this repository proposes, to be ratified rather than assumed:
**Answered 2026-09-09 by `GH-DEC-2026-012`.** The proposal below was confirmed
in full. Recorded as proposed-then-ratified rather than rewritten as though it
had always been settled:
- **informed-decision owns it** — the surface, the presentation record, the
browser client, and the evidence of informedness.
@ -117,13 +124,20 @@ The answer this repository proposes, to be ratified rather than assumed:
and statute §6.4.
- It supplies exactly one PIP-like fact — *what was presented* — as a claim
carrying `view_hash`, issuer and freshness, and it never evaluates that fact.
- The catalog row does not exist yet. `INFD-WP-0001-T02` asks `gate-house` and
writes `layer.yaml` from the ruling. If `gate-house` places this component
elsewhere, `INTENT.md` and this file change to match — the ruling wins.
- **Ruled:** PEP-shaped, confirmed as proposed. No second catalog row for the
presentation claim — PEP and PIP are shapes a repository has. The claim is
permitted under three limits (presentation-only; never an input to the
decision it presents for; evidence reaches `audit-core` independently), and
`view_hash` versus the binding digest is settled as distinct attestations with
an authority rule, linked by co-reference and never by nesting.
`layer.yaml` and `pep-stance.yaml` declare it; `INTENT.md` carries the
reasoning.
## Definition of done
1. `gate-house` has ruled on the layer placement and `layer.yaml` reflects it.
1. ~~`gate-house` has ruled on the layer placement and `layer.yaml` reflects
it.~~ **Done 2026-09-09**`GH-DEC-2026-012`; `layer.yaml`,
`pep-stance.yaml` and `tests/test_layer_conformance.py`.
2. `key-cape` has the `client_id` and callback URI, and `KEY-WP-0013-T02` is
unblocked with the answer traceable to this repository.
3. The four specs exist, are reviewed against the current `approval-engine`,

View file

@ -7,7 +7,7 @@ date: "2026-09-09"
domain: infotech
topic_slug: netkingdom
layer: surface
role: pep-shaped # PROVISIONAL — see "Layer Placement", INFD-WP-0001-T02
role: pep-shaped # confirmed by GH-DEC-2026-012; see layer.yaml
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md
companion: net-kingdom/SECURITY-COMPANION.md
---
@ -184,21 +184,69 @@ not failure** — a memo sent back for improvement is the surface working.
presented view plus explicit highlight acknowledgment — never keystroke
analytics, dwell timers, or attention theater.
## Layer Placement — Provisional
## Layer Placement — Ruled
Under security layer model v0.7 this repository is **not** an Engine. It is
browser-facing and it causes a protected side effect on the far side of a
decision, which is the shape statute §6.4 and companion §5 call **PEP-shaped**.
`approval-engine`'s own INTENT names this expectation: *"Callers that do are
PEP-shaped and owe companion §5 / statute §6.4."*
**PEP-shaped**, confirmed by `GH-DEC-2026-012` (answering `INFD-IN-0001`).
Not an Engine: this repository holds no state another layer reads at runtime for
a verdict, and renders no decision. Companion §5 is owed and statute §6.4
applies in full. The declaration is `layer.yaml`, in this repository's own voice
— a layer someone else states about you is not a declaration.
This is declared **provisional** because the catalog row does not exist yet and
inventing one is the drift §17 exists to prevent. `INFD-WP-0001-T02` takes the
question to `gate-house` and writes `layer.yaml` from the answer, not from this
paragraph. The framing this repository will argue: informed-decision is a
**presentation and binding surface** — a PEP for the acts it fronts, and a PIP
for exactly one fact, *what was presented*, which it supplies as a claim and
never evaluates.
The ruling also confirmed that emitting a **presentation claim** does not
require a second catalog row: PEP and PIP are shapes a repository has, and §4
records the layers it occupies. That permission carries three limits, and they
are the substance of it rather than caveats on it:
1. **The claim carries presentation and nothing else.** It must never carry,
restate, summarise or imply the decision, the verdict, or whether the act was
permitted. A consumer learns from it only what was *shown*, never what was
*decided*.
2. **The claim must not be an input to the decision it presents for.** A policy
reading `view_hash` to decide whether an act is permitted would let the
presenting surface contribute to its own authorization.
3. **The evidence copy reaches `audit-core` independently of this repository.**
The claim endpoint and the evidence path are different things and neither
substitutes for the other.
Limit 2 is load-bearing. `GH-DEC-2026-012` accepted this repository's argument
that a renderer attesting its own rendering is not the self-dealing that kept
the approval object out of `access-engine` — *but only because that limit
holds*. Without it, the two collapse into the same failure.
Limit 3 is the one that most shapes the architecture: audit evidence is
protected from the actor being audited, and here the actor and the source are
the same component.
### `view_hash` and the binding digest
Ruled as option (b): they are **distinct attestations with an authority rule**.
- `approval-engine`'s binding digest is authoritative for **what the request
is**.
- `view_hash` is authoritative for **what was shown**, and nothing else.
- Neither may be substituted for the other.
- **A disagreement between them is a finding against the presenting surface,
never a fact about the request.**
They link by **co-reference**, not nesting: the presentation record carries the
approval or binding identifier explicitly, and both attestations are read
against that one reference. This repository must never recompute or restate
`approval-engine`'s binding digest in its own vocabulary — it references the
digest that layer computed and recorded.
Nesting was refused for a reason worth carrying: it reproduces the hash cycle
that made `GH-DEC-2026-008` unimplementable, where a claim had to name the
digest of a request that would come to contain it, and a fail-closed consumer
obeying it would deny permanently.
### An inherited gap, stated rather than glossed
`GH-DEC-2026-010` requires a decision be **attributable** to `access-engine`.
No consumer can satisfy that today: the decision envelope is unsigned, a
declared §13 gap tracked as `FLEX-WP-0024`. This repository's record can show
that a decision was obtained and what it said; it cannot yet show that
`access-engine` said it. The decision path must not be described as validated
while that is open.
## What Would Make This Repository Wrong

Binary file not shown.

View file

@ -0,0 +1,56 @@
"""The shipped unreachable-engine stance map.
This is the map the surface actually applies. ``pep-stance.yaml`` publishes it,
and ``tests/test_layer_conformance.py`` asserts the two are equal a published
map that may drift from the code invites reliance it cannot support.
Built to v0.8 obligation 3 per GH-DEC-2026-012: the axis is enumerated rather
than defaulted, ``unknown`` resolves to ``fail_closed``, and an absent scope is
distinguishable in the record from an unknown one.
"""
from __future__ import annotations
from enum import Enum
AXIS = "binding_level"
#: Total by enumeration, not by catch-all. See ``pep-stance.yaml`` for why every
#: stance is ``fail_closed`` here where ops-warden can justify ``fail_open``.
STANCE: dict[str, str] = {
"acknowledgment": "fail_closed",
"organizational": "fail_closed",
"aes": "fail_closed",
"qes": "fail_closed",
"unknown": "fail_closed",
"absent": "fail_closed",
}
#: Values of the axis proper — the two non-value outcomes are not axis values.
AXIS_VALUES: tuple[str, ...] = ("acknowledgment", "organizational", "aes", "qes")
class BindingLevelState(str, Enum):
"""How the axis value was obtained. Recorded; never collapsed.
``ABSENT`` and ``UNKNOWN`` resolve to the same stance but must never be
recorded as the same fact: collapsing them hides a schema-drift incident
inside a malformed-input statistic.
"""
PRESENT = "present"
ABSENT = "absent"
UNKNOWN = "unknown"
def resolve(binding_level: str | None) -> tuple[str, BindingLevelState]:
"""Return ``(stance, state)`` for a memo's ``binding_level``.
There is no per-call discretion and no implicit default: an unlisted value
is resolved explicitly to the ``unknown`` stance, never permissively.
"""
if binding_level is None or binding_level == "":
return STANCE["absent"], BindingLevelState.ABSENT
if binding_level not in AXIS_VALUES:
return STANCE["unknown"], BindingLevelState.UNKNOWN
return STANCE[binding_level], BindingLevelState.PRESENT

184
layer.yaml Normal file
View file

@ -0,0 +1,184 @@
# informed-decision — NetKingdom security layer declaration
#
# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md
# Companion: net-kingdom/SECURITY-COMPANION.md v0.2
# Voice: INTENT.md (this repository's own, per §11 "who must declare")
# Ruling: GH-DEC-2026-012 (gate-house@0a1d1d9) answered INFD-IN-0001
#
# Reference form: ops-warden's, adopted by audit-core and kings-guard, with
# kings-guard's adaptation for a repository with no Tooling contacts.
#
# GH-DEC-2026-012 R1 confirmed the SHAPE. The layer is declared here, in this
# repository's own voice, because a layer someone else states about you is not
# a declaration.
schema_version: "0.1"
framework: netkingdom-security-layer-model
standard_version: "0.7"
companion_version: "0.2"
repository: informed-decision
layer: surface
role: pep-shaped
declared_by: INTENT.md
declared_at: "2026-09-09"
ruling: GH-DEC-2026-012
# §6.4 — informed-decision is PEP-shaped: it causes a protected side effect on
# the far side of a decision (recording an approver entry against an approval
# object). Companion §5 is owed and §6.4 applies in full.
#
# Built to v0.8 obligation 3, not v0.7, per GH-DEC-2026-011 — see pep-stance.yaml.
pep_stance: pep-stance.yaml
protected_action: "Approver entry recorded against an approval object (POST /v1/approvals/{id}/entries)"
decision_engine: access-engine
# §6 — no repository other than access-engine exposes an authorization decision.
# This surface renders a question and records a human's answer. A disposition is
# evidence of an act, never a verdict.
decision_surfaces_exposed: none
# §3.3 / GH-DEC-2026-012 R2 — YES to a presentation claim, and NO second catalog
# row: PEP and PIP are shapes a repository has; §4 records the layers it
# occupies. The permission carries three limits, and they are the substance of
# it rather than caveats on it.
presentation_claim:
emitted: true
carries: presentation-only
limits:
- id: L1-presentation-only
rule: >-
The claim carries presentation and nothing else. It MUST NOT carry,
restate, summarise or imply the decision, the verdict, or whether the
act was permitted. A consumer learns from it only what was SHOWN, never
what was DECIDED.
- id: L2-not-an-input
rule: >-
The claim MUST NOT be an input to the decision it presents for. A policy
reading view_hash to decide whether an act is permitted would let the
presenting surface contribute to its own authorization.
note: >-
Load-bearing, not a formality. GH-DEC-2026-012 accepted this
repository's argument that a renderer attesting its own rendering is not
the self-dealing that kept the approval object out of access-engine —
but only because this limit holds. Without it the two collapse into the
same failure.
- id: L3-independent-evidence-path
rule: >-
The evidence copy reaches audit-core INDEPENDENTLY of this repository.
The claim endpoint and the evidence path are different things and
neither substitutes for the other. The copy that is evidence MUST NOT be
reachable only through the party it is evidence about.
note: >-
The limit that matters most here: audit evidence is protected from the
actor being audited, and in this component the actor and the source are
the same. Architecture consequence, tracked in
docs/specs/ArchitectureBlueprint.md.
# §17 — the shared request-claim schema is still unowned. This repository
# publishes at its own boundary and yields to that schema when it exists.
# Position accepted by GH-DEC-2026-012 and matching approval-engine's in
# APPROVAL-IN-0001.
request_claim_schema:
status: unowned-upstream
local_shape: published-at-own-boundary
yields_to: taxonomy-request-claim-schema
# GH-DEC-2026-012 R3 — (b), with the authority rule written down.
binding_digest_relationship:
ruling: GH-DEC-2026-012
view_hash_authoritative_for: what-was-shown
binding_digest_authoritative_for: what-the-request-is
binding_digest_owner: approval-engine
substitutable: false
disagreement_is: >-
A finding against the presenting surface, never a fact about the request.
linkage: co-reference
linkage_rule: >-
The presentation record carries the approval or binding identifier
explicitly, and both attestations are read against that one reference. This
repository MUST NOT recompute or restate approval-engine's binding digest
from its own vocabulary — it references the digest that layer computed and
recorded.
nesting_forbidden: >-
view_hash MUST NOT contain the binding digest, and MUST NOT travel inside
hashed request material while containing it. Option (c) was refused because
nesting reproduces the hash cycle that made GH-DEC-2026-008 unimplementable:
a claim required to name the digest of a request that would come to contain
it, where a fail-closed consumer denies permanently.
# §5 applies to Staff. This is a browser-facing surface with no Tooling contact.
tooling_contacts: []
# §11 — record non-Tooling clients so the check is total.
non_tooling_clients: []
intended_non_tooling_clients:
- target: approval-engine
layer: engine
rationale: >-
GET /v1/approvals/{id} and /claim (approval:read) to render; POST
/v1/approvals/{id}/entries (approval:approve) to record a binding. Never
/consume. Requirements: approval-engine/docs/approver-surface-requirements.md.
- target: access-engine
layer: engine
rationale: >-
Decision consumed before rendering an approval to a person. A 200 from
approval-engine is not entitlement. This surface consumes a decision and
never renders one.
- target: key-cape
layer: engine
rationale: >-
Identity. Authorization-code + PKCE browser client. Identity is imported,
never invented here.
- target: audit-core
layer: engine
rationale: >-
Evidence destination for presentation records and dispositions. Must be an
independent path per limit L3-independent-evidence-path.
- target: state-hub
layer: not-catalogued
rationale: >-
Progress events. Outside §5 by the v0.5 scope rule. Recorded, not policed.
# §9.6 — presentation evidence is load-bearing: it is the only record of what a
# human was shown before binding. Atomicity and attestation cover accident and
# later tampering, never a compromised source.
evidence:
kind: load-bearing
residual: compromised-surface-presents-x-attests-y
residual_closed: false
custody: same-bound-as-every-other-source # §16 decided: no stronger archive
note: >-
GH-DEC-2026-012 states the residual is not closed in those words, and this
repository is not credited with closing it. Same disposition as
approval-engine's equivalent residual for adversarial omission at a
compromised source.
# INHERITED DECLARED GAP — GH-DEC-2026-010.
#
# Obligation 1 now requires a decision be ATTRIBUTABLE to access-engine. No
# consumer can satisfy that today: flex-auth's decision envelope is unsigned.
# This is a declared §13 gap tracked as FLEX-WP-0024, not a clean path this
# repository can walk.
#
# Stated here, and in SCOPE.md and ArchitectureBlueprint.md, because
# GH-DEC-2026-012 requires it be said in this repository's own documents rather
# than describing validation as complete.
inherited_gaps:
- id: GH-DEC-2026-010-attributability
obligation: 1
gap: >-
A decision consumed from access-engine cannot today be proven to have come
from access-engine — the envelope is unsigned.
tracked_by: FLEX-WP-0024
consequence_here: >-
This surface's record can show that a decision was obtained and what it
said. It cannot yet show it was access-engine that said it. Validation of
the decision path MUST NOT be described as complete while this is open.
status: open
declared_shapes:
"5.1": []
"5.2": []
"5.3": []

110
pep-stance.yaml Normal file
View file

@ -0,0 +1,110 @@
# informed-decision — PEP unreachable-engine stance map
#
# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md §6.4, §9.3, §9.7
# Built to: v0.8 obligation 3 (GH-DEC-2026-011), not v0.7 — per GH-DEC-2026-012,
# so this is built to rather than migrated to.
# Ruling: GH-DEC-2026-012
# Validate: pytest tests/test_layer_conformance.py
#
# §6.4 obligation 3 requires a declared unreachable-engine stance that is total,
# scoped per zone, carries no implicit default and no per-call discretion, and is
# published rather than held in code comments. A map that lives only in a module
# default is not published — it is merely written down.
#
# The property that makes this worth reading: it is asserted equal to the shipped
# map in informed_decision/stance.py by tests/test_layer_conformance.py. A
# published map that may drift from the code is worse than none, because it
# invites reliance it cannot support.
schema_version: "0.1"
framework: netkingdom-security-layer-model
standard_version: "0.7"
built_to_obligation_3_of: "0.8"
repository: informed-decision
pep_shape: true
declared_by: layer.yaml
ruling: GH-DEC-2026-012
protected_action: "Approver entry recorded against an approval object"
decision_engine: access-engine
# v0.8 obligation 3: the map MUST enumerate its axis rather than lean on a
# catch-all. The axis here is the memo's binding_level — this repository's own
# graded property, and the one that determines how costly a wrong bind is.
axis: binding_level
axis_source: decision-memo.schema.json#/properties/binding_level
# Total by enumeration, not by catch-all. Every value of the axis appears
# explicitly, plus the two non-value outcomes required to be distinguishable.
#
# Every stance is fail_closed, and that is a conclusion rather than a shortcut.
# ops-warden can justify fail_open on some zones because refusing removes
# production host access to close a documentation gap — a continuity argument.
# No such argument exists here. Refusing to render or to bind delays a decision;
# it does not remove a capability anyone depends on to keep a system running.
# Binding a human's identity to an act without an authorization decision is the
# precise failure this repository exists to prevent, so there is no level at
# which proceeding-without-a-decision is the safer error.
stance:
acknowledgment: fail_closed
organizational: fail_closed
aes: fail_closed
qes: fail_closed
unknown: fail_closed # v0.8 obligation 3: unknown MUST resolve to fail_closed
absent: fail_closed # distinct from unknown in the record — see below
# v0.8 obligation 3 requires an absent scope to be DISTINGUISHABLE in the record
# from an unknown one. They resolve to the same stance here but must never be
# recorded as the same fact:
# absent — the memo carried no binding_level at all (a defect in the memo)
# unknown — a binding_level was present but is not a value this map enumerates
# (a version skew between memo and this map)
# Collapsing them would hide a schema-drift incident inside a malformed-input
# statistic.
scope_absence:
distinguishable: true
recorded_as:
absent: "binding_level.absent"
unknown: "binding_level.unknown"
# §6.4 obligation 1 requires a decision record for a protected side effect.
# Where the engine is unreachable there is no decision to hold, so this surface
# records the APPLICATION OF THE STANCE instead.
on_apply:
recorded_fields:
- memo_id
- memo_version
- binding_level
- binding_level_state # present | absent | unknown
- stance_applied
- decision_id # only where a decision was actually rendered
- decision_attributable # see inherited gap below
- presentation_id
user_visible: >-
The approver is told the act could not be authorized and that nothing was
bound. The surface never presents a fail_closed outcome as a decline by the
approver: no disposition is recorded, because the human did not make one.
# GH-DEC-2026-010, inherited. Obligation 1 requires a decision be ATTRIBUTABLE to
# access-engine, and no consumer can satisfy that today — flex-auth's envelope is
# unsigned (declared §13 gap, FLEX-WP-0024). `decision_attributable` is therefore
# recorded as false and MUST NOT be reported as satisfied while that is open.
inherited_gap:
id: GH-DEC-2026-010-attributability
decision_attributable_today: false
tracked_by: FLEX-WP-0024
# §13.1 / GH-DEC-2026-011 — a dated classification-coverage figure beside the
# stance. Honest figure for a repository at spec stage: the axis is fully
# enumerated against the schema, and no protected action has shipped yet.
classification_coverage:
as_of: "2026-09-09"
axis_values_enumerated: 4
axis_values_in_schema: 4
coverage: "4/4 (100%) of binding_level values enumerated"
protected_actions_shipped: 0
note: >-
Coverage is complete against the schema, not against production behaviour:
nothing is deployed. Re-date this figure when the walking skeleton
(INFD-WP-0001-T08) ships, and treat a coverage figure that predates a schema
change as stale.

View file

@ -0,0 +1,182 @@
"""Layer and stance conformance.
GH-DEC-2026-012 confirmed this repository is PEP-shaped and told it to build to
v0.8 obligation 3 rather than migrate to it later. These tests pin the parts of
that obligation a test can actually hold:
- the published map equals the shipped map (obligation 3, a MUST);
- the axis is enumerated, not defaulted;
- ``unknown`` resolves to ``fail_closed``;
- an absent scope is distinguishable in the record from an unknown one;
- the inherited GH-DEC-2026-010 attributability gap is declared open rather
than described as satisfied.
"""
from __future__ import annotations
import pathlib
import pytest
yaml = pytest.importorskip("yaml")
from informed_decision.stance import AXIS, AXIS_VALUES, STANCE, BindingLevelState, resolve
ROOT = pathlib.Path(__file__).resolve().parents[1]
def load(name: str) -> dict:
return yaml.safe_load((ROOT / name).read_text(encoding="utf-8"))
@pytest.fixture(scope="module")
def stance_doc() -> dict:
return load("pep-stance.yaml")
@pytest.fixture(scope="module")
def layer_doc() -> dict:
return load("layer.yaml")
# --------------------------------------------------------------------------
# Obligation 3 — published equals shipped. This is the MUST.
# --------------------------------------------------------------------------
def test_published_stance_equals_shipped_stance(stance_doc):
assert stance_doc["stance"] == STANCE, (
"pep-stance.yaml has drifted from informed_decision/stance.py. "
"A published map that may differ from the code is worse than none."
)
def test_published_axis_equals_shipped_axis(stance_doc):
assert stance_doc["axis"] == AXIS
# --------------------------------------------------------------------------
# Obligation 3 — totality by enumeration, no catch-all, no implicit default.
# --------------------------------------------------------------------------
def test_every_axis_value_has_an_explicit_stance():
for value in AXIS_VALUES:
assert value in STANCE, f"{value} has no declared stance"
def test_stance_map_has_no_entries_beyond_the_axis_and_the_two_outcomes():
allowed = set(AXIS_VALUES) | {"unknown", "absent"}
assert set(STANCE) == allowed
def test_unknown_resolves_to_fail_closed(stance_doc):
"""v0.8 obligation 3 makes this a MUST; v0.7 permitted fail_open."""
assert STANCE["unknown"] == "fail_closed"
assert stance_doc["stance"]["unknown"] == "fail_closed"
@pytest.mark.parametrize("value", sorted(STANCE))
def test_no_stance_is_permissive(value):
"""Not required by the standard — required by this repository.
Binding an identity without an authorization decision is the failure this
surface exists to prevent, so there is no level at which proceeding is the
safer error. If this test is ever relaxed, the reasoning in pep-stance.yaml
must be rewritten first.
"""
assert STANCE[value] == "fail_closed"
# --------------------------------------------------------------------------
# Obligation 3 — absent must be distinguishable from unknown IN THE RECORD,
# even though both resolve to the same stance.
# --------------------------------------------------------------------------
def test_absent_and_unknown_resolve_the_same_but_record_differently():
absent_stance, absent_state = resolve(None)
unknown_stance, unknown_state = resolve("notalevel")
assert absent_stance == unknown_stance == "fail_closed"
assert absent_state is BindingLevelState.ABSENT
assert unknown_state is BindingLevelState.UNKNOWN
assert absent_state != unknown_state
def test_empty_string_is_absent_not_unknown():
assert resolve("")[1] is BindingLevelState.ABSENT
@pytest.mark.parametrize("value", AXIS_VALUES)
def test_known_axis_values_record_as_present(value):
stance, state = resolve(value)
assert state is BindingLevelState.PRESENT
assert stance == "fail_closed"
def test_published_map_declares_the_two_states_distinguishable(stance_doc):
absence = stance_doc["scope_absence"]
assert absence["distinguishable"] is True
assert absence["recorded_as"]["absent"] != absence["recorded_as"]["unknown"]
# --------------------------------------------------------------------------
# Inherited gap — GH-DEC-2026-010. Must be declared open, not glossed.
# --------------------------------------------------------------------------
def test_attributability_gap_is_declared_open_in_the_stance(stance_doc):
gap = stance_doc["inherited_gap"]
assert gap["decision_attributable_today"] is False
assert gap["tracked_by"] == "FLEX-WP-0024"
def test_attributability_gap_is_declared_open_in_the_layer(layer_doc):
gaps = {g["id"]: g for g in layer_doc["inherited_gaps"]}
gap = gaps["GH-DEC-2026-010-attributability"]
assert gap["status"] == "open"
def test_stance_records_whether_the_decision_was_attributable(stance_doc):
assert "decision_attributable" in stance_doc["on_apply"]["recorded_fields"]
# --------------------------------------------------------------------------
# R1 / R2 / R3 — the ruling's limits are declared, not merely remembered.
# --------------------------------------------------------------------------
def test_layer_declares_pep_shaped_and_no_decision_surface(layer_doc):
assert layer_doc["role"] == "pep-shaped"
assert layer_doc["decision_surfaces_exposed"] == "none"
def test_presentation_claim_carries_all_three_limits(layer_doc):
limits = {limit["id"] for limit in layer_doc["presentation_claim"]["limits"]}
assert limits == {
"L1-presentation-only",
"L2-not-an-input",
"L3-independent-evidence-path",
}
def test_binding_digest_relationship_is_co_reference_not_nesting(layer_doc):
rel = layer_doc["binding_digest_relationship"]
assert rel["linkage"] == "co-reference"
assert rel["substitutable"] is False
assert "nesting_forbidden" in rel
def test_residual_is_declared_not_closed(layer_doc):
assert layer_doc["evidence"]["residual_closed"] is False
# --------------------------------------------------------------------------
# §13.1 / GH-DEC-2026-011 — a dated coverage figure beside the stance.
# --------------------------------------------------------------------------
def test_classification_coverage_is_dated_and_complete_against_the_axis(stance_doc):
cov = stance_doc["classification_coverage"]
assert cov["as_of"]
assert cov["axis_values_enumerated"] == cov["axis_values_in_schema"] == len(AXIS_VALUES)

View file

@ -94,7 +94,7 @@ Two corrections made during the same task, recorded rather than silently fixed:
```task
id: INFD-WP-0001-T02
status: progress
status: done
priority: high
state_hub_task_id: "4f94134b-2260-5404-84e0-12f2b08ef565"
```