{% extends "base.html" %} {% from "_macros.html" import rate, score %} {% block title %}{{ run_id }} — bpmn-evalkit{% endblock %} {% block body %}
{{ summary.sut_name }} {{ summary.sut_version }} · {{ summary.dataset }} ({{ summary.dataset_version }}, {{ summary.cases }} cases) · k={{ summary.k }} · started {{ summary.started }}
{% endif %}{{ status.error }}
{% endif %} {% if metrics %}{% endif %}| Metric | Rate | Passed | Decided | Skipped |
|---|---|---|---|---|
| {{ gate }} | {{ rate(e.get('rate')) }} | {{ e.get('passed', 0) }} | {{ e.get('decided', 0) }} | {% if e.get('skipped') %}{{ e.skipped }}{% else %}0{% endif %} |
Reading soundness:
{% for k, v in (aggregates.get('soundness.verdicts') or {}).items() %}{{ k }}: {{ v }}{% if not loop.last %}, {% endif %}{% endfor %}.
Models that are not workflow nets — more than one start or end event, which is legitimate
BPMN — are reported by gate.wfnet and are never counted as unsound.
| Type | F1 | F1 median | Precision | Recall | F1 (parsed only) | Attainable | {% if has_ceiling %}Human ceiling | {% endif %}n/a |
|---|---|---|---|---|---|---|---|---|
| {{ label }} | {{ score(e.get('f1_mean')) }} | {{ score((e.get('f1') or {}).get('median')) }} | {{ score(e.get('precision_mean')) }} | {{ score(e.get('recall_mean')) }} | {{ score((e.get('valid_only') or {}).get('f1_mean')) }} | {{ score(e.get('attainable_f1_mean')) }} | {% if has_ceiling %}{{ score((aggregates.get('struct.ceiling.' + bucket) or {}).get('f1_mean')) }} | {% endif %}{% if e.get('not_applicable') %}{{ e.not_applicable }}{% else %}0{% endif %} |
Human ceiling — what two gold models of the same description score against each other, over {{ aggregates.get('struct.ceiling.pairs', 0) }} reference pair(s), measured with the same matcher and threshold. {% if refs.get('min_grade') is not none %}Only models graded ≥ {{ refs.min_grade }} counted as gold; a lower threshold would be a strictly easier benchmark.{% endif %} It is a second number to read beside the first — not a target and not a divisor. A system above the ceiling has not beaten the humans; more likely it resembles one reference closely while the references disagree among themselves.
{% endif %}Matcher {{ match.get('matcher') }}, threshold {{ match.get('threshold') }}.
Both are part of the number — a score computed at another threshold is a different score and must
not be compared with this one. The F1 column is the mean over all outputs: one that produced
nothing scores 0, not "not measured". Where it diverges from the median, the gap is the failure rate.
Subtype agreement among matched pairs:
{{ rate((aggregates.get('struct.subtype_agreement') or {}).get('rate')) }} — an exclusive gateway
matched to a parallel one counts as a match and as a disagreement.
Skipped. {{ s2.get('detail') or 'no gold models available at scoring time' }}. Skipped is not zero and not a pass: nothing was measured.
{% endif %} {% set behav = aggregates.get('behav.status') %} {% if behav %}| Question | Metric | Mean | Median | {% if has_bceiling %}Human ceiling | {% endif %}
|---|---|---|---|---|
| {{ label }} | {{ metric }} | {{ score(e.get('mean')) }} | {{ score(e.get('median')) }} | {% if has_bceiling %}{{ score((aggregates.get(metric.replace('footprint','ceiling')) or {}).get('mean')) }} | {% endif %}
The ceiling is the caveat here, and it is severe. Two gold models of the same description score {{ score((aggregates.get('behav.ceiling.f1') or {}).get('mean')) }} against each other over {{ aggregates.get('behav.ceiling.pairs', 0) }} pair(s). A relation needs both endpoints matched, so the label-matching bottleneck compounds quadratically — where humans use different vocabularies this metric is dominated by activity matching rather than by behaviour.
{% endif %}Compared model to model — the reference's Petri net is the behavioural specification, so no event log and no play-out sampling is involved. Scored on {{ behav.scored }} of {{ behav.total }} outputs {% set skipped = [] %}{% for k, v in (behav.by_status or {}).items() %}{% if k != 'scored' %}{% set _ = skipped.append(k ~ ': ' ~ v) %}{% endif %}{% endfor %} {% if skipped %}({{ skipped | join(', ') }}){% endif %}. Both directions are reported and never read alone: fitness by itself rewards a model that permits everything, precision by itself one that permits almost nothing. An unconvertible model is counted, never scored 0 — that is about pm4py's BPMN coverage, not the system. This stage inherits the stage-2 matching and therefore its threshold.
{% endif %} {% set ns = namespace(any=false) %} {% for bucket, label in [('task','Activities'),('event','Events'),('gateway','Gateways'),('lane','Lanes')] %} {% if aggregates.get('struct.subtype.confusion.' + bucket) %}{% set ns.any = true %}{% endif %} {% endfor %} {% if ns.any %}| Type | Agreement | Substitution (reference → output) | Count |
|---|---|---|---|
| {{ label if loop.first else '' }} | {{ rate(agreement.get('rate')) if loop.first else '' }} | {{ key.split('→')[0] }} → {{ key.split('→')[1] }} | {{ count }} |
| {{ label }} | {{ rate(agreement.get('rate')) }} | none |
Read reference → output: what gold had, then what the system produced. These are not interchangeable errors — an exclusive gateway modelled as a parallel one turns a decision into a fork; an inclusive modelled as exclusive narrows it. Sequence flows are excluded (they match structurally and always agree). Subtype is the XML tag, so a message event substituted for a timer is invisible here.
{% endif %} {% set band = aggregates.get('struct.sensitivity.thresholds') %} {% if band %}| Type | {% for t in band %}{{ '%g'|format(t) }} | {% endfor %}Spread |
|---|---|---|
| {{ label }} | {% for t in band %}{{ score(curve.get('%g'|format(t))) }} | {% endfor %}{{ '%.3f'|format(aggregates.get('struct.sensitivity.spread.' + bucket)) if aggregates.get('struct.sensitivity.spread.' + bucket) is not none else 'n/a' }} |
| ↳ human ceiling | {% for t in band %}{{ score(ceil.get('%g'|format(t))) }} | {% endfor %}
How much of a number is the threshold. Not an invitation to pick a better one — moving the threshold to improve a score is the failure this table exists to expose. A conclusion that holds across the whole band is a finding; one that appears only at the run's own threshold is an artefact of that number.
{% endif %}| Error-free outputs | {{ rate((aggregates.get('gate.lint.error_free') or {}).get('rate')) }} |
| Clean (warnings included) | {{ rate((aggregates.get('gate.lint.clean') or {}).get('rate')) }} |
| Errors per output (median) | {{ (aggregates.get('gate.lint.errors') or {}).get('median') }} |
| {{ r }} | {{ c }} |
A headline count is routinely dominated by one rule that encodes a design decision in the system under test rather than a defect. Always read this table first.
{% endif %}{{ aggregates.get('norm.rebuilt_flow_links', 0) }} <incoming>/<outgoing>
links rebuilt across {{ aggregates.get('norm.outputs_needing_relink', 0) }} output(s) before any gate ran.
Not a defect score — a signal about interoperability with tooling outside this harness.
| Case | Rep | Gates | Output |
|---|---|---|---|
| {{ case.case_id }} | {{ rep.rep }} | {% for gid, g in rep.gates.items() %} {% if g.outcome == 'fail' %}{{ gid.replace('gate.','') }} {% elif g.outcome == 'skipped' %}{{ gid.replace('gate.','') }}: skipped {% endif %} {% endfor %} {% if rep.error %}no output{% endif %} | {% if not rep.error %} raw · normalised {% else %}{{ rep.error }}{% endif %} |