pytest-given Self-Report
pytest 9.1.1 pytest-given 0.4.1
Module:
A scenario failing on a pytest-given refusal points at the test, not at pytest-given
✓ passed 58ms
Given
a suite that nests across phases and mistypes a term
    from pytest_given import Glossary, given, scenario, when

    g = Glossary()
    g.actor('Guest')

    @scenario('nested across phases')
    def test_nest():
        with given('a machine'):
            with when('it brews'):
                pass

    @scenario('a mistyped term')
    def test_lookup():
        with given('a guest'):
            g['Gust']
When
the suite runs with --given-json
Then
both scenarios fail
each failure ends on its own test function
A test without `@scenario` stays out of the report
✓ passed 44ms
Given
a suite whose only test is undecorated
When
the suite runs with --given-json
Then
the test itself passes
the report holds no scenario
A step fixture is grafted in as a given step
✓ passed 44ms
Given
a scenario consuming a step fixture
import pytest
from pytest_given import scenario, given, then

@pytest.fixture
@given("a prepared value")
def value():
    return 42

@scenario("Fixture test")
def test_fixture(value):
    with then(f"value is {value}"):
        assert value == 42
When
the suite runs with --given-json
Then
the test passes
the step from the fixture leads the recorded steps
The cases of a parametrized scenario become one scenario with a parameter table
✓ passed 43ms
Given
a parametrized scenario over two cases
import pytest
from pytest_given import scenario, given, when, then

@scenario("Param test", tags=["math"])
@pytest.mark.parametrize("a,b,expected", [(1, 2, 3), (2, 3, 5)])
def test_add(a, b, expected):
    with given(t"a={a} and b={b}"):
        pass
    with then(t"sum is {expected}"):
        assert a + b == expected
When
the suite runs with --given-json
Then
both cases pass
the two runs collapse into one scenario
the parameter table holds a param column per argument
it holds one row per case, with that row's values
the grouped steps carry a placeholder per matching name
A refusal on a run with no sink does not claim a report was skipped
✓ passed 26ms
Given
a suite whose narration varies across parametrize cases
When
the suite runs with no --given-* sink
Then
the refusal is reported without claiming a report was skipped
A refused run discards the previous run's report
✓ passed 22ms
Given
a suite whose narration varies across parametrize cases
import pytest
from pytest_given import scenario, when

@scenario("Brew")
@pytest.mark.parametrize("cup_size", [200, 350])
def test_brew(cup_size):
    with when(f"it brews {cup_size} ml"):
        assert cup_size > 0
a report on disk from a previous run
When
the suite runs with those sinks configured
Then
the run says no report was written, naming the sink
the stale files are gone rather than left reading as current
An unknown source link preset stops the run before it collects
✓ passed 10ms
Given
a suite that would otherwise pass
from pytest_given import scenario, then

@scenario("Brew")
def test_brew():
    with then("it brews"):
        assert True
When
the suite runs with a misspelled source link preset
Then
the run ends as a usage error, naming the flag the user typed
no test ran
An unknown source link preset in an ini reports the ini name
✓ passed 13ms
Given
a suite configured through the ini rather than the flag
When
the suite runs with an HTML sink
Then
the error names the ini setting, not a flag the user never typed
A report that fails to render discards the previous one too
✓ passed 42ms
Given
a suite with one scenario
a report pair on disk from a previous run
When
the run trips a renderer failure
Then
the run fails, saying no report was written
neither the stale pair nor a half-written new one survives
A fixture failing in teardown fails its finished scenario
✓ passed 43ms
Given
a scenario whose fixture raises after its yield
import pytest
from pytest_given import scenario, given, then

@pytest.fixture
def resource():
    yield 1
    raise RuntimeError("teardown boom")

@scenario("Teardown-failed")
def test_a(resource):
    with given("a resource"):
        value = resource
    with then("it is one"):
        assert value == 1
When
the suite runs with --given-json
Then
pytest counts the test passed and its teardown an error
the report marks the scenario failed with the teardown error
A step fixture refuses steps and attachments in its teardown
✓ passed 77ms
Given
a step fixture that adds a {late} after its yield
suite
When
the suite runs
Then
the test passes but its teardown errors with a PytestGivenError
late suite
step
suite
import pytest
from pytest_given import scenario, given, then, attach

@pytest.fixture
@given("a thing")
def thing():
    yield 1
    with given("a late step"): pass

@scenario("Teardown raises")
def test_use(thing):
    with then("it is one"):
        assert thing == 1
attachment
suite
import pytest
from pytest_given import scenario, given, then, attach

@pytest.fixture
@given("a thing")
def thing():
    yield 1
    attach("late", "data")

@scenario("Teardown raises")
def test_use(thing):
    with then("it is one"):
        assert thing == 1
A fixture takes only @given, never @when or @then
✓ passed 78ms
Given
a fixture decorated with @{decorator}
suite
When
the suite runs
Then
the decorator is {outcome}
a refusal is a PytestGivenError that points at @given
decorator suite outcome
given accepted
suite
import pytest
from pytest_given import scenario, then, given

@pytest.fixture
@given("a coin is inserted")
def coin():
    return 2

@scenario("uses the fixture")
def test_use(coin):
    with then("the coin is 2"):
        assert coin == 2
when refused
suite
import pytest
from pytest_given import scenario, then, when

@pytest.fixture
@when("a coin is inserted")
def coin():
    return 2

@scenario("uses the fixture")
def test_use(coin):
    with then("the coin is 2"):
        assert coin == 2
then refused
suite
import pytest
from pytest_given import scenario, then, then

@pytest.fixture
@then("a coin is inserted")
def coin():
    return 2

@scenario("uses the fixture")
def test_use(coin):
    with then("the coin is 2"):
        assert coin == 2
A scenario that fails as expected keeps its steps, error and reason under its own status
✓ passed 44ms
Given
a scenario marked xfail whose body fails
import pytest
from pytest_given import scenario, given, then

@scenario("Planned")
@pytest.mark.xfail(reason="not implemented yet")
def test_planned():
    with given("a value"):
        value = 1
    with then("it is two"):
        assert value == 2
When
the suite runs with --given-json
Then
pytest counts the test xfailed
the report marks the scenario xfailed with its reason, its steps and the error that broke it
An editor source link opens the file under the rootdir, wherever pytest runs from
✓ passed 153ms
Given
a suite whose test sits in a subdirectory, run from inside it
When
the HTML report renders with the vscode preset
Then
the link names the test file's path from the rootdir
A scenario is matched against each of its stories
✓ passed 43ms
Given
a scenario binding two stories whose sentence its narration fits
from pytest_given import Glossary, scenario, sentence, story, when

g = Glossary()
guest = g.actor('Guest')
search = g.activity('search')
room = g.work_object('Room')
a = story('Book', [sentence(guest, search, room)])
b = story('Stay', [sentence(guest, search, room)])

@scenario('both', stories=[a, b])
def test_both():
    with when(t'the {guest} does a {search} for a {room}'):
        pass
When
the suite runs with --given-json
Then
the test passes
the scenario binds both stories and covers the sentence of each
A declared story no scenario covers appears in the report
✓ passed 43ms
Given
a suite declaring a story that no scenario names or pins
from pytest_given import Glossary, given, scenario, sentence, story

g = Glossary()
guest = g.actor('Guest')
search = g.activity('search')
room = g.work_object('Room')
story('Unread', [sentence(guest, search, room)])

@scenario('x')
def test_x():
    with given('something'):
        pass
When
the suite runs with --given-json
Then
the report lists the story, its sentence covered by nothing
A pinned scenario still counts its step pins
✓ passed 50ms
Given
a scenario pinning one sentence, whose steps pin a second and narrate a third
from pytest_given import Glossary, given, scenario, sentence, story

g = Glossary()
guest = g.actor('Guest')
search = g.activity('search')
room = g.work_object('Room')
s = story('Book', [
    sentence(guest('Alice'), search, room),
    sentence(guest('Bob'), search, room),
    sentence(guest, search, room)])

@scenario('x', stories=s, pins=s[1])
def test_x():
    with given('a pinned step', pins=s[2]):
        pass
    with given(t'the {guest} does a {search} for a {room}'):
        pass
When
the suite runs with --given-json
Then
the scenario covers both pinned sentences and not the one its narration would match
A step pin into a story outside stories= covers it
✓ passed 46ms
Given
a step pinning a story its scenario does not name
from pytest_given import Glossary, given, scenario, sentence, story

g = Glossary()
guest = g.actor('Guest')
search = g.activity('search')
room = g.work_object('Room')
a = story('Book', [sentence(guest, search, room)])
b = story('Stay', [sentence(guest, search, room)])

@scenario('x', stories=a)
def test_x():
    with given('thing', pins=b[1]):
        pass
When
the suite runs with --given-json
Then
the scenario passes and covers the pinned sentence, in the story it did not name
A wide fixture recording keeps its pins in every scenario it is grafted into
✓ passed 46ms
Given
a module-scoped step fixture pinning a sentence, set up first by an unannotated test
import pytest
from pytest_given import Glossary, given, scenario, sentence, story

g = Glossary()
guest = g.actor('Guest')
search = g.activity('search')
room = g.work_object('Room')
a = story('Book', [sentence(guest, search, room)])
b = story('Stay', [sentence(guest, search, room)])

@pytest.fixture(scope='module')
@given('a module-scoped arrangement')
def wide():
    with given('an inner step', pins=a[1]):
        pass
    yield 1

def test_unannotated(wide):
    assert wide == 1

@scenario('first', stories=a)
def test_first(wide):
    pass

@scenario('second', stories=b)
def test_second(wide):
    pass
When
the suite runs with --given-json
Then
every test passes, and both scenarios cover the pinned sentence, whichever story they name
An Annotated label carrying a pin pins its step
✓ passed 43ms
Given
a scenario whose Annotated given(...) label on a plain fixture carries a pin
from typing import Annotated
import pytest
from pytest_given import Glossary, given, scenario, sentence, story

g = Glossary()
guest = g.actor('Guest')
search = g.activity('search')
room = g.work_object('Room')
s = story('Book', [sentence(guest, search, room)])

@pytest.fixture
def room_number():
    return 7

@scenario('x', stories=s)
def test_x(room_number: Annotated[int, given('a room', pins=s[1])]):
    pass
When
the suite runs
Then
the label's step carries the pin
An Annotated label retells the pins of the fixture label it replaces
✓ passed 132ms
Given
a label with pins={label_pins} over a fixture pinning a[1]
When
the suite runs
Then
the grafted root pins the sentences {root_sentences} of a
the inner step keeps its own pin, a[3]
label_pins root_sentences
None [1]
[] []
[a[2]] [2]
An Annotated Template label on an unparametrized scenario fails that scenario
✓ passed 90ms
Given
a Template label on a plain fixture parameter
from typing import Annotated
import pytest
from pytest_given import scenario, given, when, Template

@pytest.fixture
def room():
    return 101

@scenario('a room is booked')
def test_it(room: Annotated[int, given(Template('room {room} is free'))]):
    with when('it is booked'):
        pass
When
the suite runs with an HTML report
Then
the scenario errors, naming the parameter and the fix
the HTML report is still written
An Annotated Template label fails its scenario unless its placeholder is a bare parametrize column
✓ passed 118ms
Given
a parametrized scenario whose Template label on a plain fixture holds {placeholder}
suite
When
the suite runs
Then
the scenario errors, saying {error}
placeholder suite error
{x} None
suite
from typing import Annotated
import pytest
from pytest_given import scenario, given, when, Template

@pytest.fixture
def room():
    return 101

@scenario('bad')
@pytest.mark.parametrize('x', [1])
def test_it(x, room: Annotated[int, given(Template('room {x}'))]):
    with when('it is booked'):
        pass
{room} '{room}' in the Annotated label on parameter 'room' does not match
suite
from typing import Annotated
import pytest
from pytest_given import scenario, given, when, Template

@pytest.fixture
def room():
    return 101

@scenario('bad')
@pytest.mark.parametrize('x', [1])
def test_it(x, room: Annotated[int, given(Template('room {room}'))]):
    with when('it is booked'):
        pass
{room.number} bare identifiers
suite
from typing import Annotated
import pytest
from pytest_given import scenario, given, when, Template

@pytest.fixture
def room():
    return 101

@scenario('bad')
@pytest.mark.parametrize('x', [1])
def test_it(x, room: Annotated[int, given(Template('room {room.number}'))]):
    with when('it is booked'):
        pass
A bare run writes no report at all
✓ passed 27ms
Given
a suite with one scenario
    import pytest
    from pytest_given import scenario, given, when, then

    @scenario('Buy coffee')
    def test_buy():
        with given('a machine'):
            pass
        with when('I insert money'):
            pass
        with then('I get coffee'):
            assert True
When
the suite runs with no output flag
Then
the run passes
no report directory appears in the default location
A bare `--given-md` prints the narration to stdout
✓ passed 37ms
Given
a suite with one scenario
When
the suite runs with a bare --given-md
Then
the narration is printed between the fence markers
`--given-html` alone writes no JSON report
✓ passed 75ms
Given
a suite with one scenario
When
the suite runs with --given-html alone
Then
the HTML rendering is written
no JSON lands in the default location
A sink flag pointed at a source file is refused before the suite runs
✓ passed 10ms
Given
a suite with one scenario
When
a bare --given-html swallows the test path that follows it
Then
the run is refused, naming the path and the flag-order fix
the source file is left exactly as it was, not overwritten
A rejected authoring form fails the run and writes no report
✓ passed 22ms
Given
a suite whose narration varies across parametrize cases
import pytest
from pytest_given import scenario, when

@pytest.mark.parametrize('cup_size', [200, 350])
@scenario('Brew')
def test_brew(cup_size):
    with when(f'the machine brews {cup_size} ml'):
        pass
When
the suite runs with all three sinks configured
Then
the run fails, naming the offending form
not one sink is written, and no traceback escapes
`--given-title` names the report instead of the rootdir
✓ passed 41ms
Given
a suite with one scenario
When
the suite runs with --given-title
Then
the test passes
the title reaches the JSON metadata
the title also heads the Markdown rendering
`--given-theme` sets the theme the HTML report opens in
✓ passed 154ms
Given
a suite with one scenario
When
the suite runs with --given-theme=dark
Then
the test passes
the page declares dark as its default theme
An unknown theme stops the run before it collects
✓ passed 10ms
Given
a suite that would otherwise pass
When
the suite runs with a misspelled theme, and no HTML sink
Then
the run ends as a usage error, naming the flag the user typed
no test ran
A run with no sink still enforces the grouping rules
✓ passed 21ms
Given
a suite whose f-string narration records no parts
import pytest
from pytest_given import scenario, then

@scenario("Brew")
@pytest.mark.parametrize('cup_size', [200, 300])
def test_brew(cup_size):
    with then(f'it brews {cup_size} ml'):
        assert cup_size
When
the suite runs with no sink configured
Then
the run still fails, naming the offending form
Narration lint is off unless it is asked for
✓ passed 22ms
Given
a suite with one flawed step
from pytest_given import scenario, given, when, then

@scenario("Empty given")
def test_empty_given():
    with given("a value"):
        pass
    with when("computing"):
        x = 2
    with then("it is two"):
        assert x == 2
When
the suite runs without the lint flag
Then
the run passes and says nothing about the lint
no step source is recorded, so the AST surface costs nothing
An error-severity finding fails the run
✓ passed 27ms
Given
a suite whose given step has an empty body
from pytest_given import scenario, given, when, then

@scenario("Empty given")
def test_empty_given():
    with given("a value"):
        pass
    with when("computing"):
        x = 2
    with then("it is two"):
        assert x == 2
When
the suite runs with the lint enabled
Then
the run exits failed, naming the lint rule and the step
A lint rule downgraded to warn reports without failing the run
✓ passed 21ms
Given
a suite whose given step has an empty body
from pytest_given import scenario, given, when, then

@scenario("Empty given")
def test_empty_given():
    with given("a value"):
        pass
    with when("computing"):
        x = 2
    with then("it is two"):
        assert x == 2
When
the suite runs with that lint rule set to warn
Then
the run still passes
the finding is printed anyway
Either narration lint flag overrides the ini for one run
✓ passed 40ms
Given
a suite with one flawed step
from pytest_given import scenario, given, when, then

@scenario("Empty given")
def test_empty_given():
    with given("a value"):
        pass
    with when("computing"):
        x = 2
    with then("it is two"):
        assert x == 2
When
the suite runs with the lint enabled by ini but off by flag
Then
the lint does not run
When
the suite runs with the lint disabled by ini but on by flag
Then
the lint runs and its error finding fails the run
An error finding leaves a more specific exit code alone
✓ passed 20ms
Given
a suite whose lint would fail, under a stale ignore entry
from pytest_given import scenario, given, when, then

@scenario("Empty given")
def test_empty_given():
    with given("a value"):
        pass
    with when("computing"):
        x = 2
    with then("it is two"):
        assert x == 2
When
the suite runs deselected, so nothing is collected
Then
the run keeps NO_TESTS_COLLECTED rather than reporting a test failure
A failure inside the lint keeps the report it was handed
✓ passed 80ms
Given
a clean suite and a lint pass that raises
When
the suite runs with an HTML sink
Then
the failure is summarized as the lint's, rather than raised as a traceback or titled as a report that was not written
the report that was already written is still there
A scenario records under its node ID
✓ passed 0ms
Given
a fresh collector
When
a scenario starts under its Node ID and finishes
Then
it carries its Node ID, name, status and tag
A scenario is timed from past its step fixture setup
✓ passed 0ms
Given
a collector whose clock reads 100.3s once setup is done
When
the clock is started past setup and the body runs 0.2s
Then
the recorded duration is the body alone, not the setup before it
Steps record with their phases
✓ passed 0ms
Given
an active scenario in a fresh collector
When
a given and a when step are pushed
Then
each step carries its phase
Steps pushed during fixture setup record into the fixture recording
✓ passed 0ms
Given
a fixture recording under setup
When
a step is pushed inside the fixture body
Then
it is recorded as a child of the recording root
An attachment lands on the step being recorded
✓ passed 0ms
Given
a fixture recording under setup
When
an attachment is attached inside the fixture body
Then
the attachment lands on the recording root
Fixture-body steps do not leak into the active scenario
✓ passed 0ms
Given
an active scenario with a fixture recording
When
a step is pushed inside the fixture body
Then
the step lives only in the recording, not the scenario
An attachment outside every step is refused
✓ passed 0ms
Given
an active scenario with no step open
When
an attachment is made from the test body
Then
it is refused rather than dropped
A fixture recording is deep-copied when grafted
✓ passed 0ms
Given
a fixture recording with a nested child step
When
a graft copies it into the active scenario
Then
the scenario gains a deep copy of the recorded steps
The collector fails a scenario that already finished
✓ passed 0ms
Given
a scenario that already finished as passed
When
the collector is told of a failure after that
Then
the recorded scenario carries the failure
A teardown failure keeps the error the scenario already carries
✓ passed 0ms
Given
a scenario that already failed in its body
When
its fixture then also fails in teardown
Then
the body failure is what the report shows
A collector reports which node ids it recorded
✓ passed 0ms
Given
a collector that recorded one scenario
When
the recorded and an unrecorded node id are both asked about
Then
only the recorded node id is claimed
A leaf given is grafted as a childless given step
✓ passed 0ms
Given
an active scenario is being recorded
When
a leaf graft appends a childless step
Then
the step is a given with no children
Grafting with an override replaces the root label but keeps children
✓ passed 0ms
Given
a fixture recording whose root has a label and a child
When
a graft supplies an override narration
Then
the grafted root shows the override text and keeps its children
Grafting with no active scenario is refused
✓ passed 0ms
Given
a collector with no active scenario
When
a leaf graft runs
Then
the invariant is asserted rather than silently dropping the step
FileGlossary lookup is case-insensitive
✓ passed 0ms
Given
a file glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
the same term is looked up in three different cases
Then
every lookup resolves to one handle type and the same id
Repeated lookups return the same handle
✓ passed 0ms
Given
a file glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
the same term is looked up twice
Then
both lookups return the one memoized handle
File-loaded terms start kindless
✓ passed 0ms
Given
a Markdown glossary file with no kind column
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
a file glossary loads it
Then
each term is kindless until kind inference runs
An unknown name raises with a suggestion
✓ passed 1ms
Given
a file glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
a misspelt term is looked up
Then
a PytestGivenError is raised with a spelling hint
Handles are usable inline in a sentence
✓ passed 1ms
Given
a file glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
its handles build a sentence
Then
each slot becomes a term ref
Calling a handle overrides its display
✓ passed 0ms
Given
a file glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
a handle is called to name an instance
Then
the term ref carries the overridden display
An explicit kind column sets term kinds
✓ passed 3ms
Given
a Markdown glossary with an explicit Kind column
| Term | Meaning | Kind |
|---|---|---|
| Guest | x | Actor |
| Room | y | Work Object |
| book | z | Activity |
When
the file glossary reads the Kind column
Then
kinds come straight from the file, not kind inference
A kind column can be selected by integer index
✓ passed 3ms
Given
a Markdown glossary with the kind in the third column
| Term | Meaning | Kind |
|---|---|---|
| Guest | x | Actor |
| Room | y | Work Object |
When
the file glossary selects the kind column by index
Then
the kinds are read from that column
A work_object kind alias maps to the object kind
✓ passed 3ms
Given
a glossary whose Kind cell says work_object
| Term | Meaning | Kind |
|---|---|---|
| Room | y | work_object |
When
the file glossary parses the kind
Then
it normalizes to the work object kind
An unrecognized kind value is rejected
✓ passed 1ms
Given
a glossary whose Kind cell holds an unknown value
| Term | Meaning | Kind |
|---|---|---|
| Guest | x | Wizard |
When
the file glossary loads the file
Then
a PytestGivenError names the unrecognized kind
A missing glossary file is reported clearly
✓ passed 0ms
Given
a path to a file that does not exist
When
a file glossary is opened on that path
Then
a PytestGivenError reports the file is not found
A term cell with no alphanumeric characters is rejected
✓ passed 1ms
Given
a row whose term cell has no id-able characters
| Term | Meaning |
|---|---|
| @#$ | some definition |
When
the file glossary loads the file
Then
a PytestGivenError is raised with file:line context
A file glossary error about its tables names the file
✓ passed 3ms
Given
a glossary file with {problem}
that file on disk as bad.md
Glossary file
When
a file glossary loads it
Then
a PytestGivenError names the file before the problem: {message}
problem Glossary file message
no table bad.md: found no Markdown pipe table
Glossary file
# no table here
a short row bad.md: data row at line 3
Glossary file
| Term | Meaning |
|---|---|
| Guest |
no Term column bad.md: column 'Term'
Glossary file
| Word | Meaning |
|---|---|
| Guest | x |
Duplicate rows for one term collapse only when identical
✓ passed 4ms
Given
two rows for one term, with identical definitions: {identical}
Glossary file
When
the file glossary loads the file
Then
the rows yield {outcome}
a refusal points at the second row as the conflict
identical Glossary file outcome
True one term
Glossary file
| Term | Meaning |
|---|---|
| Guest | First definition. |
| Guest | First definition. |
False refused
Glossary file
| Term | Meaning |
|---|---|
| Guest | First definition. |
| Guest | Second definition. |
A blank description normalizes to undefined
✓ passed 1ms
Given
a row whose description cell is blank
| Term | Meaning |
|---|---|
| Guest |   |
When
the file glossary parses it
Then
the term definition is None, i.e. undefined
Calling FileGlossary looks up a known term
✓ passed 1ms
Given
a file glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
a known term is looked up by call
Then
a deferred term is returned
FileGlossary is a closed vocabulary
✓ passed 0ms
Given
a file glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
an unknown name is called
Then
a PytestGivenError is raised
no new term was created
Term ids are derived as URL-safe slugs, and a name with none is refused
✓ passed 0ms
Given
the name {text}
When
it is slugified into a term id
Then
a PytestGivenError reports the derived id is empty: {refused}
the id is {slug}
text refused slug
Guest False 'guest'
Order received False 'order-received'
Work Object False 'work-object'
do_the_thing False 'do-the-thing'
Buy / sell False 'buy-sell'
Guest #1 False 'guest-1'
café False 'caf'
booking system False 'booking-system'
--- True None
True None
True None
### True None
Calling an actor names a distinct instance
✓ passed 0ms
Given
an actor handle for Guest
When
the actor is called with a name
Then
an instance with a distinct display is returned
Calling an activity records an inflection of the same term
✓ passed 0ms
Given
an activity handle for confirm
When
the activity is called with a surface form
Then
an inflection sharing the activity identity is returned
Registering an actor returns a typed handle
✓ passed 0ms
Given
an empty glossary
When
an actor is registered with a definition
Then
a handle carrying the actor kind is returned
Re-registering a term is idempotent only with matching fields
✓ passed 0ms
Given
an actor already registered with a definition
When
the same name is registered again, with the same definition: {same_definition}
Then
the re-registration yields {outcome}
a refusal reports the conflict with the prior registration
same_definition outcome
True one shared term
False refused
The same name cannot be two different kinds
✓ passed 0ms
Given
a name already registered as an actor
When
the same name is registered as an activity
Then
a PytestGivenError reports the conflict with the prior registration
Registering an actor captures its definition site
✓ passed 0ms
Given
a rootdir-aware glossary
When
an actor is registered
Then
the term records a source link to this file
Calling the glossary declares a kindless term
✓ passed 0ms
Given
an empty glossary
When
a term is declared by call, without a kind
Then
the term is registered as kindless
Subscript looks up an already-declared term
✓ passed 0ms
Given
a glossary with one declared term
When
the name is looked up by subscript
Then
the returned term is the declared one
Subscripting an unknown name raises with a hint
✓ passed 0ms
Given
a glossary with one declared term
When
a near-miss name is subscripted
Then
a PytestGivenError is raised with a spelling hint
A kindless term takes its kind from the slots it fills
✓ passed 0ms
Given
a kindless term filling the {slots} slots, one story each
When
kind inference runs over the stories
Then
a conflict naming the term is reported: {conflict}
the inferred kind is {kind}
slots conflict kind
[] False None
['actor'] False actor
['verb'] False activity
['noun'] False object
['actor', 'noun'] False actor
['verb', 'actor'] True None
['verb', 'noun'] True None
A declared kind must fit the slot its term fills
✓ passed 0ms
Given
a term declared as {declared}
a story putting it in the {slot} slot
When
kind inference runs over the story
Then
the declared kind is {outcome}
a refusal names the declared kind and the slot
declared slot outcome
actor actor kept
activity actor refused
object actor refused
actor verb refused
activity verb kept
object verb refused
actor noun kept
activity noun refused
object noun kept
A term named only in the second clause of a sentence gets its kind inferred
✓ passed 0ms
Given
a glossary of kindless term entries
a sentence whose second clause starts at another actor
When
kind inference runs over its story
Then
the first term of the second clause is an actor
A conflict error names only the offending stories
✓ passed 0ms
Given
an actor term that also appears in a verb slot
When
kind inference runs over both stories
Then
a PytestGivenError reports the conflict
only the offending story is named in the message
A conflict message excludes stories with an unrelated slot
✓ passed 0ms
Given
a kindless term used in verb, actor and noun slots
When
kind inference runs over all three stories
Then
a PytestGivenError reports the verb-vs-actor conflict
only the verb and actor stories are named, not the noun one
Slot positions alternate verb/noun after the actor
✓ passed 0ms
Given
the five positions of a short clause
When
the slot rule is applied to each position
Then
position 0 is the actor slot, then verb and noun alternate
A pipe table parses into term and definition rows
✓ passed 0ms
Given
a Markdown document with one pipe table
# Glossary

| Term | Meaning |
|------|---------|
| Guest | A person booking. |
| Room  | A bookable room. |
When
the parser reads it into rows for a file glossary
Then
each row carries a term, definition and source line
Multiple tables in one file are merged
✓ passed 0ms
Given
a document containing two separate pipe tables
# Glossary

| Term | Meaning |
|------|---------|
| Guest | A person booking. |
| Room  | A bookable room. |

## More

| Term | Meaning |
|---|---|
| Search | Look up. |
When
the parser reads the whole document
Then
every table contributes its term rows
Columns can be selected by header name
✓ passed 0ms
Given
a table with custom, differently-cased header names
| Word | Note | Role |
|---|---|---|
| Guest | x | Actor |
When
the parser selects columns by header name
Then
the named columns are matched case-insensitively
Escaped pipes are preserved in cells
✓ passed 0ms
Given
cells containing escaped pipe characters (\|)
| Term | Meaning |
|---|---|
| A\|B | pipe\|here |
When
the parser splits the row
Then
the escaped pipe survives as a literal pipe
Tables inside fenced code blocks are skipped
✓ passed 0ms
Given
a fenced code block that contains a look-alike table
```
| Term | Meaning |
|---|---|
| Fake | nope |
```

| Term | Meaning |
|---|---|
| Real | yes |
When
the parser reads the document
Then
only the real table outside the fence contributes rows
A file with no pipe table is rejected
✓ passed 0ms
Given
a document with no pipe table
# Just a heading

No tables here.
When
the parser reads it for a file glossary
Then
a PytestGivenError reports that the file has no pipe table
A missing named column is rejected
✓ passed 0ms
Given
a Markdown document with one pipe table
# Glossary

| Term | Meaning |
|------|---------|
| Guest | A person booking. |
| Room  | A bookable room. |
When
the parser selects a header name that is absent
Then
a PytestGivenError names the missing column
A column index out of range is rejected
✓ passed 0ms
Given
a Markdown document with one pipe table
# Glossary

| Term | Meaning |
|------|---------|
| Guest | A person booking. |
| Room  | A bookable room. |
When
the parser selects a column index past the table width
Then
a PytestGivenError names the out-of-range column
A data row with too few columns is rejected
✓ passed 0ms
Given
a table with a data row narrower than its header
| Term | Meaning | Type |
|---|---|---|
| Guest | A person |
| Room | A bookable room | place |
When
the parser reads the short row
Then
a PytestGivenError points at the short row
A term cell is read as its plain text, with emphasis unwrapped
✓ passed 0ms
Given
a term cell written as {cell}
When
the parser reads the term cell
Then
the term is {term}
cell term
**Scenario** Scenario
*Step* Step
`given` given
work_object work_object
`a*b*c` a*b*c
Emphasis is stripped from kind cells too
✓ passed 0ms
Given
a Kind cell written with bold emphasis
| Term | Meaning | Kind |
|---|---|---|
| Guest | x | **Actor** |
When
the parser reads the kind cell
Then
the kind is unwrapped to plain text
Definition markdown is left intact
✓ passed 0ms
Given
a definition cell rich with inline code
| Term | Meaning |
|---|---|
| Scenario | A test decorated with `@scenario(...)`. |
When
the parser reads the row
Then
the definition keeps its markup for the tooltip
A pipe line without a separator is not a table
✓ passed 0ms
Given
prose containing a stray pipe, then a real table
This line has a | in it but no separator follows.
Next line is not a separator.

| Term | Meaning |
|---|---|
| Real | yes |
When
the parser reads the document
Then
only the real pipe table produces rows
A step pairs its narration with a phase
✓ passed 0ms
When
a given step descriptor is created
Then
it carries the given phase and its narration
A step opened outside a scenario warns rather than raising
✓ passed 0ms
Given
a collector recording inside an undecorated test
When
a given step is opened against it
Then
a `PytestGivenWarning` is raised, not an error
it names the missing `@scenario`, so a suite can filter it
when_then records the action and its outcome as siblings
✓ passed 0ms
Given
an active scenario in a local collector
When
a when_then block exits cleanly
Then
a when and a sibling then step are recorded
when_then pairs with an inner pytest.raises
✓ passed 0ms
Given
an active scenario in a local collector
When
the when_then body raises and an inner pytest.raises swallows it
Then
both sibling steps are still recorded
when_then omits the then when the body raises uncaught
✓ passed 0ms
Given
an active scenario in a local collector
When
the when_then body raises with nothing catching inside
Then
only the when step is recorded — the outcome never held
Inside a when_then body only a when may open, as a child of the action
✓ passed 0ms
Given
an active scenario in a local collector
When
a {phase_name} step opens inside the when_then body
Then
the inner step is {outcome}
a refusal reports the cross-phase nesting
the step stack is left balanced
phase_name outcome
when nested
given refused
then refused
`@scenario` marks the test function without wrapping it
✓ passed 0ms
Given
a test function taking one fixture
When
the function is decorated
Then
the very same function comes back, keeping its signature
it carries the scenario marker, and a plain one does not
An attachment label must be plain text
✓ passed 0ms
Given
a non-str attachment label of kind {label_kind}
When
it is attached
Then
a PytestGivenError says attachment labels are plain text
label_kind
deferred-template
t-string
not-a-string
A `Template` narration is refused in a test body
✓ passed 0ms
Given
an active scenario in a local collector
When
a {phase_name} step opens on a `Template`
Then
a PytestGivenError says a template is not supported in a test body
phase_name
given
when
then
A sentence number or name pins only once looked up on its story
✓ passed 0ms
Given
the sentence {written}, looked up on its story: {looked_up}
When
a step is declared with it as its pin
Then
the pin is {outcome}
a refusal shows the handle form
written looked_up outcome
2 True accepted
2 False refused
cancel True accepted
cancel False refused
[1, 2] True accepted
[1, 2] False refused
An actor handle in a clause becomes a term ref
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
When
a clause is built from three glossary handles
Then
the actor slot becomes a term ref
An inflected activity keeps its term identity but shows the inflection
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
an activity handle called with an inflection
When
it takes the verb slot of a clause
Then
the term ref shows the inflection over the same activity
A bare string in a clause becomes a connective word
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
When
a clause is built with a bare word between term nodes
Then
the bare word becomes a clause part word, not a term ref
A clause needs at least an actor, an activity and a node
✓ passed 0ms
Given
a Guest actor
a search activity
When
a clause of only two parts is built
Then
a PytestGivenError rejects it as too short, counting the parts
A clause position takes only the kinds its slot accepts
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
a term declared as {kind}
When
a clause is built with it at position {position}
Then
the term is {outcome}
a refusal names the position and the declared kind
kind position outcome
actor 0 accepted
actor 1 refused
actor 2 accepted
object 0 refused
object 1 refused
object 2 accepted
activity 0 refused
activity 1 accepted
activity 2 refused
None 0 accepted
None 1 accepted
None 2 accepted
A bare string may fill any slot of a clause, as a word
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
the bare string "plain"
When
a clause is built with it at position {position}
Then
it becomes a clause part word there
the other parts stay term refs
position
0
1
2
A clause may be fully bare words
✓ passed 0ms
Given
three plain words with no glossary handles
When
a clause is built from them
Then
every part is a clause part word
Node/edge alternation allows a trailing connective node
✓ passed 0ms
Given
an actor, an activity, a work object and a second actor
When
they form a five-part clause joined by a connective
Then
even positions are term-ref nodes and the connective stays a word
A clause may not end on a dangling edge
✓ passed 0ms
Given
an actor, activity and work object plus a connective
When
a clause ending on a connective edge is built
Then
a PytestGivenError names the trailing arrow with no target
A single-clause sentence synthesizes one clause
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
When
a sentence is built from handles directly
Then
it wraps a single clause
A sentence may hold several clauses
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
two clauses
When
they are combined into one sentence
Then
the sentence carries both clauses
Mixing loose parts and prebuilt clauses is rejected
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
a prebuilt clause
When
it is combined with loose handles in one sentence
Then
a PytestGivenError rejects the mix
A story auto-numbers its sentences from one
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
When
a story is built from two sentence rows
Then
the sentences are numbered 1 and 2
A story derives its id from its title
✓ passed 0ms
Given
a human-readable story title
When
a story is built from it
Then
its id is the slugified title
A story may span only one glossary
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
two sentences that reach two different glossaries
When
a story is built spanning both glossaries
Then
a PytestGivenError says a story spans multiple glossaries
A sentence handle is looked up by name or by number
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
a story whose second sentence is named
When
the sentence is looked up by its name and by its number
Then
both handles name sentence 2 of that story
Iterating a story yields its sentence handles
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
a story of two sentences
When
the story is iterated
Then
it yields each sentence handle in order
Looking up a sentence the story lacks lists the ones it has
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
a story with an unnamed and a named sentence
When
an unknown name is looked up
Then
a PytestGivenError lists the story's sentences
Two sentences of one story cannot share a name
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
two sentences both named "cancel"
When
a story is built from them
Then
a PytestGivenError names the duplicate and both numbers
A sentence name must be non-empty and unpadded
✓ passed 0ms
Given
a Guest actor
a search activity
a Room work object
the sentence name {name}
When
a sentence is built with that name
Then
the name is {outcome}
a refusal says what a name must be
name outcome
'cancel' accepted
'' refused
' cancel' refused
'cancel ' refused
Two stories with the same id collide
✓ passed 0ms
Given
a story already declared under an id
When
a second story is declared with the same slug
Then
a PytestGivenError reports the id was already declared
A clause may chain a second verb-object pair
✓ passed 0ms
Given
an actor, two activity and two work object handles
When
they form a five-node clause (actor verb object verb object)
Then
every slot is a term ref, with no bare words
A declared work object in a verb slot is rejected at construction
✓ passed 1ms
Given
a file glossary declaring Room a work object
When
Room is placed in the verb slot
Then
a PytestGivenError names the term and its declared kind
A slot error names the term, not its repr
✓ passed 0ms
Given
a Guest actor
a Room work object
a search activity
When
a work object is placed in the verb slot
Then
the message names the term without dumping the glossary
the message is short and free of dataclass reprs
A non-handle clause part names its type
✓ passed 0ms
Given
a Guest actor
a Room work object
When
an int is passed where an activity handle belongs
Then
a PytestGivenError names the offending type and the clause
A Template parses a bare placeholder
✓ passed 0ms
Given
a deferred `Template` with one placeholder
When
the template is parsed
Then
it splits into literal and placeholder narration parts
A Template substitutes parametrize values
✓ passed 0ms
Given
a `Template` referencing a parametrize column
When
a parametrize value is substituted in
Then
the placeholder is filled with that value
A Template accepts bare identifiers only
✓ passed 0ms
Given
the placeholder {text}
When
a `Template` is built from it
Then
the placeholder is {outcome}
a refusal says bare identifiers only
text outcome
count={count} accepted
count={obj.attr} refused
{d[key]} refused
{x + 1} refused
A t-string interpolation becomes a value part
✓ passed 0ms
Given
a t-string step with one interpolated value
When
the t-string is parsed at runtime
Then
the interpolation becomes a narration value part
A t-string can interpolate an arbitrary expression
✓ passed 0ms
Given
a t-string step interpolating a computed expression
When
the t-string is parsed
Then
the value highlight part records the full expression
A glossary handle in a t-string emits a term ref showing what the handle was called with, else the canonical name
✓ passed 0ms
Given
the {name} handle from the glossary, called with {called_with}
When
the handle is interpolated into a t-string step
Then
the step carries one term ref, to {term_id}
it shows {display}
name called_with term_id display
Guest None guest Guest
Guest Alice guest Alice
Room None room Room
Room Deluxe Suite room Deluxe Suite
search None search search
search searches for search searches for
A term ref may not carry a format spec
✓ passed 0ms
Given
an actor handle interpolated with a format spec
When
the t-string is parsed
Then
a PytestGivenError says a term ref takes no format spec
A FileGlossary handle works in a t-string step
✓ passed 0ms
Given
a deferred term from a file glossary
When
it is interpolated into a t-string step
Then
the step carries a single term ref
Narration lint flags a step whose body does nothing
✓ passed 4ms
Given
a given step whose body is only `pass`
def test_a():
    with given('a value'):
        pass
When
the AST rules parse that source
Then
an empty-step finding points at the step line
its severity is error
Narration lint flags a then step that checks nothing
✓ passed 1ms
Given
a then step whose body only calls
def test_a():
    with then('it is one'):
        x = compute()
        handlers[0](x)
When
the AST rules parse that source
Then
a then-without-check finding reports the unchecked then
Narration lint flags an assert outside a then step
✓ passed 7ms
Given
a {phase} step whose body asserts
step body
When
the AST rules parse that source
Then
a warn finding names the {phase} step holding the assert: {flagged}
phase step body flagged
given True
step body
def test_a():
    with given('a stocked machine'):
        machine = stock()
        assert machine['coffees'] > 0
when True
step body
def test_a():
    with when('a stocked machine'):
        machine = stock()
        assert machine['coffees'] > 0
then False
step body
def test_a():
    with then('a stocked machine'):
        machine = stock()
        assert machine['coffees'] > 0
Narration lint flags a then step that folds in the action
✓ passed 3ms
Given
a scenario with no when, acting inside its then
def test_a():
    with given('a machine'):
        machine = stock()
    with then('it brews'):
        assert brew(machine) == 'coffee'
When
the AST rules parse that source
Then
a warn finding points at the then and says no when acts
Narration lint flags a narration interpolating a name the body never uses
✓ passed 3ms
Given
a given step whose body never loads the name
def test_a():
    with given(t'a {size} ml cup'):
        cup = make_cup()
When
the AST rules parse that source
Then
a warn finding names the interpolation the body ignores
Narration lint flags a passed scenario that skips a phase
✓ passed 0ms
Given
a passed scenario narrating only given and then
When
the runtime rules run
Then
one missing-phase finding names the absent when and the scenario source
its severity is the catalog default, warn
Narration lint flags a tag that duplicates a term
✓ passed 0ms
Given
a glossary defining one term
two scenarios carrying that word as a tag
When
the runtime rules run
Then
a single warn finding names the tag and the term it shadows, counting the scenarios and naming one
Narration lint flags a term referenced by no scenario name, step or story
✓ passed 0ms
Given
a glossary holding one unreferenced term
When
the runtime rules run over no scenarios and no stories
Then
the finding names the unreferenced term
its severity is off — the rule is opt-in
Narration lint counts a term named only in the second clause of a sentence as referenced
✓ passed 0ms
Given
a story whose one sentence names the term only in its second clause
When
the runtime rules run over that story
Then
dead-term flags none of its terms
A sentence is referenced by its terms, whatever their surface form
✓ passed 0ms
Given
a sentence written with an instance and an inflection
When
coverage collects the sentence references
Then
they are the term ids alone; words contribute nothing
A multi-clause sentence unions references across its clauses
✓ passed 0ms
Given
a sentence with two clauses
When
coverage collects the sentence references
Then
the terms of both clauses are present
A sentence whose clauses start at different actors builds and is covered
✓ passed 0ms
Given
a sentence of two clauses: a guest signs the register, and a clerk signs the register
a step naming both actors, the activity and the register
When
coverage is computed against the story
Then
the sentence is covered
A step is referenced by its terms, whatever their surface form
✓ passed 0ms
Given
a step naming an instance and an inflection
When
coverage collects the step references
Then
they are the term ids alone
An instance and its bare term cover each other
✓ passed 0ms
Given
a sentence naming a bare actor
the same sentence naming an instance of that actor
a step naming the instance, and one naming the bare actor
When
coverage is computed for each pairing
Then
the instance step covers the bare sentence
the bare step covers the instance sentence
Promoting a bare word to an activity ref drops coverage from a step that matched
✓ passed 0ms
Given
a step naming two term refs
the same sentence with that middle slot a bare word, then an activity ref
When
coverage is computed against each story
Then
the two-ref sentence is covered
the widened sentence is no longer covered
A scenario pin covers exactly its sentences
✓ passed 0ms
Given
a story with a matching and an under-anchored sentence
a scenario whose step matches sentence 1 but which pins sentence 2
When
coverage is computed against the story
Then
only the pinned sentence is covered, matching never ran
A step is narration-matched only where neither it nor its scenario pins
✓ passed 0ms
Given
a scenario with pins={scenario_pins}
a step matching sentence 1, with pins={step_pins}
When
coverage is computed against the story
Then
the scenario covers the sentences {covered}
scenario_pins step_pins covered
None None [1]
None [2] [2]
[] None []
[] [2] [2]
[1] [2] [1, 2]
A sentence is coverage-eligible only with two distinct terms
✓ passed 0ms
Given
a sentence referencing the terms {term_ids}
When
its coverage eligibility is checked
Then
it is eligible: {eligible}
term_ids eligible
['guest', 'room'] True
['guest', 'guest'] False
['guest'] False
[] False
An under-anchored sentence is covered only through a pin
✓ passed 0ms
Given
a story whose sentence references the terms {term_ids}
a step narrating those terms, pinning the sentence: {pinned}
When
coverage is computed against the story
Then
the sentence is covered: {covered}
term_ids pinned covered
['guest', 'room'] False True
['guest', 'room'] True True
['guest'] False False
['guest'] True True
Nested steps are walked for coverage
✓ passed 0ms
Given
a story with one canonical sentence
the covering term refs in a nested child step
When
coverage is computed against the story
Then
the nested step still counts and the sentence is covered
The glossary view aggregates instances and activity forms
✓ passed 0ms
Given
a report whose story and scenario reference entity instances and an inflection
{
  "metadata": {
    "project": "p",
    "timestamp": "t",
    "pytest_version": "8",
    "plugin_version": "0",
    "commit_sha": null,
    "title": null
  },
  "scenarios": [
    {
      "id": "t",
      "narration": {
        "text": "s",
        "parts": []
      },
      "module": "m",
      "tags": [],
      "status": "passed",
      "duration_ms": 0,
      "steps": [
        {
          "phase": "when",
          "narration": {
            "text": "x",
            "parts": [
              {
                "term_id": "guest",
                "display": "Alice",
                "expression": ""
              },
              {
                "term_id": "search",
                "display": "searches",
                "expression": ""
              },
              {
                "term_id": "room",
                "display": "Deluxe Suite",
                "expression": ""
              }
            ]
          },
          "children": [],
          "attachments": [],
          "pins": null,
          "fixture_name": null
        }
      ],
      "parameters": null,
      "error": null,
      "skip_reason": null,
      "xfail_reason": null,
      "source": null,
      "story_ids": [
        "book"
      ],
      "pins": null
    }
  ],
  "glossary": {
    "terms": [
      {
        "id": "guest",
        "kind": "actor",
        "canonical": "Guest",
        "definition": null,
        "source": null
      },
      {
        "id": "room",
        "kind": "object",
        "canonical": "Room",
        "definition": null,
        "source": null
      },
      {
        "id": "search",
        "kind": "activity",
        "canonical": "search",
        "definition": null,
        "source": null
      }
    ]
  },
  "stories": [
    {
      "id": "book",
      "title": "Book",
      "sentences": [
        {
          "id": 1,
          "clauses": [
            {
              "parts": [
                {
                  "term_id": "guest",
                  "display": "Alice"
                },
                {
                  "term_id": "search",
                  "display": "searches for"
                },
                {
                  "term_id": "room",
                  "display": "Deluxe Suite"
                }
              ]
            }
          ],
          "name": null
        }
      ],
      "source": null
    }
  ]
}
When
the glossary aggregations are built
Then
the entity terms collect their instances
the activity collects its inflection but not its canonical form
Terms referenced by a sentence record the story
✓ passed 0ms
Given
a story whose sentence references an actor and an activity
When
the glossary aggregations are built
Then
the actor and the activity each list that story
A story referencing a term twice lists it once
✓ passed 0ms
Given
a story whose two sentences repeat the same term and the same inflection
When
the glossary aggregations are built
Then
the story and the inflection appear once each
A canonical entity reference is not an instance, whatever its case
✓ passed 0ms
Given
a story sentence referencing entities by canonical name, and a step referencing one in lowercase
When
the glossary aggregations are built
Then
neither entity term records an instance
A kindless term records only its story ref
✓ passed 0ms
Given
a kindless term referenced by a story sentence
When
the glossary aggregations are built
Then
the term lists the story but no instance and no inflection
An instance seen in a fixture step records its fixture provenance
✓ passed 0ms
Given
a scenario whose fixture-sourced step names an instance
When
the glossary aggregations are built
Then
the instance carries the fixture name
The term index maps each term to its scenarios once
✓ passed 0ms
Given
a scenario referencing one term in two steps and another in its name
When
the term-scenario index is built
Then
each term maps to the scenario exactly once
Parameter coloring marks placeholders and table headers
✓ passed 140ms
Given
a report holding a parametrized scenario with a parameter table
When
the renderer renders the HTML page
Then
parameter coloring classes mark the grouped placeholder and the table headers
the page carries one generated color rule per column, after the stylesheet so a term ref bound to a column takes the column ink
each column ink is a token set once per theme, so the dark theme only redefines the token
A passed scenario renders as a checked heading with step bullets
✓ passed 0ms
Given
a report holding a passed scenario with three steps
When
the Markdown report is rendered
Then
the heading is checked and each step is a phase bullet
# pytest-given — proj

## ✓ Buy coffee
`tests/t.py::test_buy` · billing, happy-path

- **given** a machine
- **when** I insert $2
- **then** I get a coffee
Nested steps indent under their parent
✓ passed 0ms
Given
a scenario whose when step has a nested child
When
the Markdown report is rendered
Then
the child bullet indents under its parent
# pytest-given — proj

## ✓ Nest
`tests/t.py::test_nest`

- **when** outer
  - **when** inner
Structured narration renders terms, values and placeholders
✓ passed 0ms
Given
a step whose narration carries a term ref, a value and a placeholder
When
the Markdown report is rendered
Then
the term ref renders in guillemets, the value verbatim and the placeholder in braces
# pytest-given — proj

## ✓ ignored
`tests/t.py::test_parts`

- **when** a «Guest»42{amount}
A parametrized scenario renders its parameter table
✓ passed 0ms
Given
a parametrized scenario with a two-case parameter table
When
the Markdown report is rendered
Then
the heading counts the cases and the parameter table lists each row
# pytest-given — proj

## ✓ Pricing · 2 cases
`tests/t.py::test_price`

- **when** insert

| euros | expect |
|---|---|
| 1 | False |
| 2 | True |
The parameter table shows a status column only when its cases differ in status
✓ passed 0ms
Given
a parameter table whose first case is {first_status} and whose second is {second_status}
When
the Markdown report is rendered
Then
the status column is {status_column}
Rendered Markdown
first_status second_status status_column Rendered Markdown
passed passed omitted
Rendered Markdown
# pytest-given — proj

## ✓ Att · 2 cases
`tests/t.py::test_att`

- **when** act

| coin |
|---|
| euro |
| token |
skipped skipped omitted
Rendered Markdown
# pytest-given — proj

## ✓ Att · 2 cases
`tests/t.py::test_att`

- **when** act

| coin |
|---|
| euro |
| token |
failed failed omitted
Rendered Markdown
# pytest-given — proj

## ✓ Att · 2 cases
`tests/t.py::test_att`

- **when** act

| coin |
|---|
| euro |
| token |
passed skipped shown
Rendered Markdown
# pytest-given — proj

## ✓ Att · 2 cases
`tests/t.py::test_att`

- **when** act

| coin | |
|---|---|
| euro | ✓ |
| token | ○ |
passed failed shown
Rendered Markdown
# pytest-given — proj

## ✓ Att · 2 cases
`tests/t.py::test_att`

- **when** act

| coin | |
|---|---|
| euro | ✓ |
| token | ✗ |
A failed scenario ends with a minimal error digest
✓ passed 0ms
Given
a failed scenario carrying a two-line error and an internal frame
{
  "message": "ValueError: not sold out\nassert 1 == 0",
  "frames": [
    {
      "path": "/x/_pytest/runner.py",
      "lineno": 1,
      "func": "run",
      "code": "",
      "is_internal": true
    },
    {
      "path": "/x/tests/test_shop.py",
      "lineno": 88,
      "func": "test_sold_out",
      "code": "buy(m)",
      "is_internal": false
    }
  ],
  "error_tail": null
}
When
the Markdown report is rendered
Then
the heading is crossed and the error follows the steps
# pytest-given — proj

## ✗ Sold out
`tests/t.py::test_sold_out`

- **then** reports sold out

> ValueError: not sold out
> test_shop.py:88 in test_sold_out
only the first message line and the non-internal frame are quoted
A multi-line attachment renders as a fenced block
✓ passed 0ms
Given
a step carrying a multi-line attachment
When
the Markdown report is rendered
Then
the attachment content sits in an indented fence, not inline
# pytest-given — proj

## ✓ Multi
`tests/t.py::test_multiline`

- **then** result
  - 📎 Doc:
    ```
    line1
    line2
    ```
A skipped scenario shows its skip reason
✓ passed 0ms
Given
a skipped scenario with a reason
When
the Markdown report is rendered
Then
the heading is marked skipped and the reason follows the node id
# pytest-given — proj

## ○ Later · skipped
`tests/t.py::test_skip` — reason: needs fixture data

- **when** act
The JSON report carries each sentence's coverage
✓ passed 0ms
Given
a story with a covered, an uncovered, an untracked sentence
When
the JSON sink is rendered
Then
a top-level `coverage` lists every sentence once
the rest of the report is the input dict, unchanged
A re-rendered report recomputes coverage rather than carrying it
✓ passed 0ms
Given
a saved report dict whose `coverage` no longer matches its steps
When
`pytest-given report` re-renders it to JSON
Then
the coverage is the one the steps actually earn
A source link config value resolves to its template: a preset name, a raw template, or `none`
✓ passed 0ms
Given
the source link config set to {value}
When
the config value is resolved
Then
the template is {template}
value template
vscode vscode://file/{path}:{line}
cursor cursor://file/{path}:{line}
zed zed://file/{path}:{line}
pycharm pycharm://open?file={path}&line={line}
https://github.com/o/r/blob/{sha}/{relpath}#L{line} https://github.com/o/r/blob/{sha}/{relpath}#L{line}
none None
An unknown preset name is refused, with the valid ones listed
✓ passed 0ms
Given
a bareword that is neither a known preset nor a template
When
the config value is resolved
Then
the value is refused
the error names the offender and lists every valid preset
The github preset prefers GITHUB_REPOSITORY over the git remote
✓ passed 0ms
Given
GITHUB_REPOSITORY naming one repository
an origin remote naming a different one
When
the github preset is resolved
Then
the template points at the environment's repository
The github preset derives org and repo from the git origin remote
✓ passed 0ms
Given
no GITHUB_REPOSITORY, and an https origin remote
When
the github preset is resolved
Then
the blob-URL template names the remote's org and repo
The github preset refuses a remote that is not on GitHub
✓ passed 0ms
Given
no GITHUB_REPOSITORY, and an origin remote on another host
When
the github preset is resolved
Then
the preset is refused
the error points at the env var and the raw-template escape hatch
An under-anchored sentence reads as untracked until a pin covers it
✓ passed 0ms
Given
a story whose only sentence is anchored by at least two distinct terms: {anchored}
a scenario whose step pins it: {pinned}
When
the story rollups are built
Then
it is coverage-eligible: {eligible}
it reads as untracked: {untracked}
anchored pinned eligible untracked
True False True False
False False False True
False True False False
A scenario bound to two stories is matched against each
✓ passed 0ms
Given
two stories each with a guest-search-room sentence
a scenario bound to both whose step names those terms
When
the story rollups are built
Then
the scenario is listed under, and covers, both stories
A sentence is labeled by the prose of its clauses
✓ passed 0ms
Given
a story with a two-clause sentence
When
the sentence labels are built
Then
the label gives the number, then reads as prose under a story-scoped key, with the clause texts joined
One xfailed case makes its parametrized scenario an expected failure, with that case's reason
✓ passed 0ms
Given
a passed case and an xfailed one with the reason "planned"
When
the grouping pass collapses them
Then
the scenario is xfailed and carries the xfailed case's reason
Grouping collapses parametrize cases into one scenario
✓ passed 0ms
Given
three case records of one parametrized scenario
When
the grouping pass collapses them
Then
one scenario remains and any failed case fails it
A parametrized scenario keeps its place among the scenarios around it
✓ passed 0ms
Given
a plain scenario between two parametrized ones
When
the grouping pass runs
Then
the report lists them in the order the file declares
Same-named parametrized scenarios on different test functions stay apart
✓ passed 0ms
Given
two test functions whose cases share one name
When
the grouping pass runs
Then
each function keeps its own scenario and parameter table
The grouped tree comes from the first passed case
✓ passed 0ms
Given
a skipped first case and a second one that ran
When
the cases are grouped
Then
the tree is the one the passed case recorded
A plain-str narration that varies across cases is refused
✓ passed 0ms
Given
two cases whose text differs but records no parts
When
the cases are grouped
Then
the grouping is refused
the error names the test, the missing parts and the t-string fix
it names the case whose values were baked in, and the per-case opt-out
A narrated value that varies becomes a derived parameter table column
✓ passed 0ms
Given
two cases narrating a value that differs
When
templatizing walks the cases
Then
the value becomes a derived column beside the parametrize one
the step keeps a placeholder pointing at that column
the placeholder keeps the format spec and conversion it narrated
A varying interpolation that is not a bare name is refused
✓ passed 0ms
Given
two cases narrating a computed expression
When
the cases are grouped
Then
the grouping is refused
the error quotes the expression and shows the bind-a-local fix
A parameter table cell reads the way the scenario name formats it
✓ passed 0ms
Given
a Template scenario name formatting its parameter
When
the cases are grouped
Then
the cells carry the formatting the name declared
A scenario name formatting a parameter a step reads plainly gets its own column
✓ passed 0ms
Given
a name formatting the parameter and a step reading it plainly
When
the cases are grouped
Then
the name points at a column holding what it renders
the name renders the disambiguated token, text and parts agreeing
A step formatting a parameter the scenario name reads plainly gets its own column
✓ passed 0ms
Given
a step formatting the parameter and a name reading it plainly
When
the cases are grouped
Then
the step points at a column holding what it renders
the step renders the disambiguated token, text and parts agreeing
A step narrating a parameter its column no longer holds is refused
✓ passed 0ms
Given
two cases narrating a value their column lacks
When
the cases are grouped
Then
the grouping is refused
the error names the column and what the case actually narrated
A term ref whose display differs between cases is refused
✓ passed 0ms
Given
two cases whose term ref reads differently
When
the cases are grouped
Then
the grouping is refused
the error names the term ref and the split-it-out fix
A term ref that *is* the parametrize value is refused too
✓ passed 0ms
Given
two cases whose term ref is the parameter itself
When
the cases are grouped
Then
the grouping is refused
the error points at the per-case scenario opt-out
A parameter table orders its columns the way the narration first shows them
✓ passed 0ms
Given
two cases whose given attaches a varying log and whose later step narrates the parameter
When
templatizing walks the cases
Then
the given's attachment column comes first
each case row follows the same order
An attachment whose payload varies becomes an attachment column
✓ passed 0ms
Given
two cases attaching a label with differing payloads
When
templatizing walks the cases
Then
the payload moves into an attachment column
the step keeps a content-less badge pointing at it
A step whose set of attachment labels differs between cases is refused
✓ passed 0ms
Given
an attachment label only one case attaches
When
the cases are grouped
Then
the grouping is refused
the error names the label, the case, and asks for a constant one
A parameter table cell reads the way the step that points at it read
✓ passed 0ms
Given
two cases narrating a parameter with a format spec
When
grouping builds the parameter table
Then
each cell carries the formatted text, under one column
the step keeps its placeholder, which that cell substitutes into
Cases that narrate different steps are refused rather than grouped
✓ passed 0ms
Given
two cases whose step trees differ
When
the cases are grouped
Then
the grouping is refused
the error names the divergence and the opt-out that answers it
A step narrating a glossary term parameter keeps pointing at its parameter table column
✓ passed 0ms
Given
a step narrating a parameter bound to a glossary term instance
When
the cases are grouped
Then
the parameter table holds the term displays alone
the step still points at that column
A parametrized scenario can decline the grouping and keep one scenario per case
✓ passed 0ms
Given
two cases of a scenario that opted out
When
the grouping pass runs
Then
each case stands alone, with no parameter table
Adopt pytest-given
12 sentences42 scenarios
Domain ExperttellsStoryto theDeveloper
tell no coverage
DevelopercapturesStoryasSentence
capture 3 scenarios
DeveloperbuildsGlossarywith theDomain Expert
build 3 scenarios
AgentwritesScenariowithTagagainst theGlossary
write 1 scenario
AgentnarratesStepwith aPhase
narrate 3 scenarios
AgentattachesAttachmentto aStep
attach 1 scenario
CollectorrecordsStepon theStep stack
record 8 scenarios
CollectorgraftsFixture recordingfrom aStep fixture
graft 6 scenarios
CollectorgroupsParametrized scenariointo aParameter table
group 5 scenarios
RendererrendersReportwithParameter coloring
render 1 scenario
Narration lintflagsScenarioagainst aLint rule
flag 11 scenarios
Domain ExpertreviewsScenarioin theReport
review no coverage
Scenarios
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Covers:
Glossary
55 terms 6 actors 16 work objects 2 activities 31 uncategorized
Actors
Collector
The per-session object accumulating scenarios, fixture recordings, and parameter info, published through a ContextVar so a nested run can displace and restore it.
1 story11 scenarios
Renderer
Converts a JSON report into a rendering — a self-contained HTML page or a Markdown document.
1 story2 scenarios
Narration lint
The --given-lint pass that checks recorded scenarios against the Lint rule catalog once the Report model is built. It enforces the structural subset of the narration conventions — it cannot check whether a Step's text is semantically true.
1 story11 scenarios
Developer
Person who writes the application code and — together with the Agent — the scenarios; curates the glossary with the Domain Expert.
1 story
Domain Expert
Person who owns the domain knowledge and the ubiquitous language; source of domain stories and reviewer of scenarios and reports. A stakeholder in the broad sense.
1 story
Agent
AI coding agent that authors and maintains scenarios alongside the Developer, guided by the pytest-given skills.
1 story
Work Objects
Scenario
A test function decorated with @scenario(...). Not every pytest test is a scenario — undecorated tests are tolerated but not collected.
1 instance1 story48 scenarios
Step
A unit of narration: a with given(...) / when(...) / then(...) block, or the root recording from a step fixture. Steps nest, and each carries a phase, narration, attachments and children — but no status or error of its own: a failure is recorded on the Scenario, and per Case on the Parameter table, which is where both renderers read it.
2 instances1 story56 scenarios
Phase
The category of a step: given, when, or then. A step has exactly one phase.
1 instance1 story3 scenarios
Tag
Free-form string label attached via @scenario(name, tags=[...]). Used by the report's filter UI, where a / nests it (ticket/ABC-123 under ticket) and selecting a prefix filters to every tag beneath it.
1 story2 scenarios
Attachment
A labeled blob (text or JSON) bound to the currently-active step via attach(label, content).
1 instance1 story8 scenarios
Parametrized scenario
A @scenario-decorated test that also carries @pytest.mark.parametrize(...). Produces multiple scenario records during a run; pytest-given groups them.
1 instance1 story9 scenarios
Instances: parametrized scenarios
Parameter table
The per-scenario grouping of typed columns + cases. A column is a param (a @pytest.mark.parametrize input), a derived (a narrated value that varies across cases) or an attachment (an attachment whose payload varies). Appears in the report below the grouped-template steps.
1 story11 scenarios
Step fixture
A pytest fixture whose function is wrapped with @given(text). Only @given is allowed on fixtures; @when / @then are rejected.
1 story4 scenarios
Fixture recording
A captured subtree of steps + attachments produced while a step fixture is being set up. Stored keyed by fixture-instance identity. A generator fixture's teardown records nothing — it refuses steps and attachments.
1 story6 scenarios
Step stack
The chain of currently-open steps; entered by with given(...), popped on exit. Mirrored inside a fixture recording while a fixture body is running.
1 story1 scenario
Report
The output artifact: a JSON data file and optional self-contained HTML and Markdown renderings derived from it. The JSON is the source of truth.
1 story24 scenarios
Parameter coloring
Each parametrize column gets a stable highlight color; placeholders and matching values share that color wherever they appear in step text and the parameter table.
1 story1 scenario
Lint rule
One named check, carrying a surface and a default Severity. A runtime rule reads the recorded scenarios; an ast rule parses the step bodies' source. The catalog in lint/base.py is data, so severities, config validation and docs stay in sync with one table.
1 instance1 story11 scenarios
Glossary
The Ubiquitous-Language concept — the shared vocabulary a domain speaks in — and the class that realizes it: Glossary(), with .actor(...), .work_object(...), .activity(...) registration methods and g('foo') (declare-or-get a kindless term, optional definition=) / g['foo'] (get-only, raises on unknown) accessor forms.
1 story12 scenarios
Story
A named flow modeled as a sequence of sentences. Constructed by story('Title', [sentence(...), ...]). Stories are first-class report tabs and the unit of coverage.
1 instance1 story35 scenarios
Sentence
What Domain Storytelling calls a sentence: an actor, an activity and its work objects, plus connective words. One row in a story, numbered by its position (1..N, never set by hand) and optionally named with sentence(..., name='cancel'), a name that stays put when rows are inserted. Constructed by sentence(...). Usually one clause, built implicitly; several when the sentence has several arrows under one number.
1 instance1 story33 scenarios
Activities
Group
Collapsing the N scenario records of a parametrized scenario into one logical scenario carrying a Parameter table. Cases group when they share both the same name and the same Node ID without its parametrize tail — one test function; two same-named scenarios on different test functions stay separate. @scenario(group_parametrized=False) declines the merge, so each case lives on as its own scenario.
1 story19 scenarios
Graft
Attaching a fixture recording into the active scenario's step tree at the moment its host test starts.
1 story5 scenarios
Uncategorized
Narration
The human-readable text on a step or scenario name. A Narration bundles the flat rendered text with parts — empty for plain-string authoring, and for a t-string or pytest_given.Template a list of NarrationLiteral / NarrationValue / NarrationPlaceholder / NarrationTermRef pieces. The structured form lets the templatizer and renderer treat parametrize-bound values specially without regex tricks.
11 scenarios
when_then
A step-authoring helper that emits a when action and its then outcome as two sibling steps from a single with block. Used mainly to narrate an expected raise (with when_then('the action', 'the error is raised'), pytest.raises(...)), so the action and its outcome stay distinct steps.
4 scenarios
Case
One row of a parametrized scenario — a single tuple of parameter values, its status, and any error.
23 scenarios
Templatize
Derive the grouped-template step text by comparing every comparable case: what all of them share stays inline, and anything that varies becomes a {name} placeholder or attachment badge pointing at a parameter-table column. The baseline tree comes from the first passed case, and every other passed case must narrate that same template.
3 scenarios
Plain fixture
A pytest fixture without a pytest-given decorator. It produces no step in the report unless an Annotated[..., given(...)] label on the test's parameter narrates it.
1 scenario
Active scenario
The scenario currently being recorded into; tracked by node ID.
11 scenarios
Node ID
A pytest test identifier (e.g., tests/test_x.py::test_y[a-b]). Used as a key throughout the collector.
2 scenarios
Value highlight
A neutral highlight applied to t-string interpolation values that don't correspond to a parametrize column and are constant across every case (e.g., a computed expression like price * 1.2). One that varies becomes a derived column instead.
1 scenario
Source link
A clickable file:line anchor on a scenario card, a story panel, or an expanded glossary term card, resolved from the given_source_link config — a preset name like vscode / github, or a raw URL template. Captured as a SourceLocation (POSIX relpath + 1-indexed line) from pytest.Item.location for a scenario, from the declaration site for a Story or Term. Disabled by default.
5 scenarios
Theme
The HTML report's colour scheme — light, dark, or auto (follow the viewer's system). The given_theme config sets the default a report opens in; a viewer's own choice from the report's theme control, once made, wins over it in that browser.
2 scenarios
Finding
One Lint rule firing on one subject: the rule id, the Severity it fired at, the subject, a Source link location and a message naming the offender.
11 scenarios
Severity
A Lint rule's level — off, warn or error — defaulted by the catalog and overridable per rule via given_lint_rules. Only error fails the run; warn reports in the terminal summary; an off rule is not evaluated at all.
4 scenarios
File glossary
A glossary loaded from a Markdown file, via the FileGlossary(path) class. It parses all GFM pipe tables in the file into the same inner Glossary model; terms are accessed by name (g['Guest'], case-insensitive). Kind inference fills in term kinds post-collection from clause slot positions when no explicit kind_column is configured.
21 scenarios
Deferred term
A term handed over before its kind is settled — what g('foo') and g['foo'] on a code glossary return, and every file glossary lookup. The handle is the same TermHandle the typed registrations (g.actor(...) and friends) hand back; declared_kind is None is what marks the deferral. The deferral is in the handing over, not the term: a row with an explicit kind_column arrives through the same handle already kinded, while the rest stay None until kind inference runs.
2 scenarios
Term
A registered glossary entry: an Actor, Work Object, Activity, or kindless term. Each carries an id (slug), a canonical name, a kind (None when kindless), and an optional definition (`str
43 scenarios
Handle
The Python object a glossary hands back for a term — TermHandle, the same type from every accessor (g.actor(...), g('foo'), g['Guest'], a captured guest = ...). It is what steps, scenario titles and sentences interpolate, in one of three surface forms: bare (room — the canonical display), `.low` (room.low — lowercased), or called (room('Deluxe Suite') — an instance on an Actor or Work Object, an inflection on an Activity). Each form renders as a term ref. A story hands out a sentence handle the same way: book_a_room['cancel'] by name or book_a_room[3] by number, which pins= takes.
1 scenario
Actor
A glossary term for a participant in the domain (e.g., Guest). Carries the actor kind color in the report.
16 scenarios
Work Object
A glossary term for a thing acted on (e.g., Room, Booking). Carries the work-object kind color in the report.
6 scenarios
Activity
A glossary term for what an actor does (e.g., book, confirm), which Domain Storytelling draws as an arrow labelled with a verb. Activities accept inflections: calling book('books') records books as a surface form of the canonical book.
10 scenarios
Term ref
An occurrence of a term inside narration. Modeled as NarrationTermRef in step text and as ClauseTermRef inside a clause.
15 scenarios
Instance
A named refinement of an Actor or Work Object (e.g., guest('Alice') is an instance of the Guest actor). Instances aggregate in the Glossary tab's refs block.
9 scenarios
Inflection
A surface form of an Activity term other than its canonical name (e.g., searches for as an inflection of search). Reported under "Also used as:" in the Glossary.
7 scenarios
Clause part
The two-variant union making up a clause's prose (ClausePart = ClauseTermRef or ClauseWord): a reference to a glossary term, whose kind resolves via the glossary, or a bare clause word — a node label or edge connective that carries no kind or id, is never classified by inference, and never reaches the glossary. That last is what separates it from a kindless term, which has an id and is tracked.
4 scenarios
Clause
One linear walk through a sentence's arrows, a node/edge alternation starting actor → verb → noun, constructed by clause(...). A sentence with several arrow chains under one number, such as an actor handing a work object to two recipients, takes one clause per chain. Domain Storytelling names no such unit.
18 scenarios
Slot
A position role in a clause, from its node/edge alternation: position 0 is the actor slot, odd positions are verb slots, and even positions ≥ 2 are noun slots. Slots drive both clause validation and kind inference.
8 scenarios
Scenario↔sentence binding
The link between a scenario (or step) and story sentences. @scenario(stories=...) names the stories a scenario is narration-matched against; a pin (pins= on @scenario or on given/when/then) binds sentences explicitly, in any story, whether stories= names it or not.
1 scenario
Pin
What a sentence handle passed to pins= is recorded as, the way a term handle in narration is recorded as a term ref: a Pin naming a story and a sentence number. A pin binds explicitly and replaces narration matching — a step pin for that step, a scenario pin (@scenario(pins=...)) for every step of the scenario, which then covers its own pins plus its steps'. pins=[] pins nothing but still opts out of narration matching. A pin reaches under-anchored sentences too.
11 scenarios
Coverage
The "did this scenario touch that sentence" relation. Computed by the A_refs ⊆ S rule: a sentence is covered when the set of terms it references is a subset of the terms a single step references — matching is per step, not against the union across steps, and on terms, not surface forms: an instance or inflection counts as its term. A pin covers its sentences directly instead, regardless of narration or term count.
14 scenarios
Kind inference
The post-collection pass (infer_glossary_kinds) that assigns each undeclared term a kind from the slot positions it occupies across all story sentences; declared kinds are verified against observed positions instead. A term used in incompatible slots (or conflicting with its declared kind) raises. A term never referenced by any sentence stays kindless.
7 scenarios
Kindless
A term with kind=None — left unset when kind inference finds no story slot to infer from (a term used only in t-string steps, never in a sentence). Shown in the report's Uncategorized bucket.
6 scenarios
Undefined
A term with definition is None; surfaced by a badge and filter in the Glossary view. Orthogonal to kindless.
1 scenario
No terms match this filter.