Skip to main content

Loadable scenarios and the runbook

Three pairs of scenarios and six singles, one task each, then the runbook that walks the whole demo in narrative order, then how to put the default branch back. Each task makes its own branch, loads its own scenario input and runs its own check, so nothing here asks you for a branch name. Every number below is produced by the engine and asserted by a unit test.

The demo guide carries the run order and links the other scenario pages.

Two loadable scenarios for the ODU layer​

Both are scenario input under demo/. They share one branch because they touch no common wavelength, and one task owns it:

uv run invoke demo-odu

It makes odu-demo if it is not there, loads both files, provisions the eleven London to Milan circuits the first one asks for, and runs the capacity check. Run it again and it continues onto the same branch.

demo/04_odu_ten_in_one.yml, ten circuits in one wavelength. Provision svc-lon-mil-sdh-01 through -10 and each maps into an ODU2 of 8 slots that grooms into the same ODU4 line container. Ten eights are eighty, so the tenth takes it to 80 of 80 and none of the ten lights a wavelength of its own. svc-lon-mil-sdh-11 is then refused, reason no-slots, naming the container it did not fit with both slot figures and then saying that no wavelength could be lit either. Grooming is tried first and lighting second, so the file has to close both escapes. It spends the last usable block on oms-fra-mil, leaving 532,400 MHz in 29 blocks. The widest of those, 38,000 MHz, is narrower than the 44,400 MHz the narrowest mode in the catalog occupies.

This one ends in an accepted refusal too. The file carries refusal_accepted: true on svc-lon-mil-sdh-11 and on none of the ten that provisioned. provisionable reads the refusal, sees it signed for and passes. The scenario's point is that grooming is tried before lighting and a refusal has to close both escapes, and none of that needs a red pipeline. The flag also survives the generator: a rerun that refuses the service again leaves the signature alone, and only the path where the service actually provisions clears it.

The band split on that branch is odu4 5, full 1, no-odu 15. Only oms-ams-lon turns red, and the two other sections the corridor crosses stay green because they still carry empty ODUC4 wavelengths at 320 free.

demo/05_odu_mixed_fill.yml, every band at once. No service and no generator run. It writes client containers directly, spread so that at least one section lands in each of the five bands. The measured split is odu4 5, odu2 1, odu0 1, full 2, no-odu 12, over 21 sections. Load it, fetch odu-map from any PoP, and the legend can be read against the picture.

Read the map, then check it. Two minutes, and it is the step that says whether the drawing works without its author standing next to it. Give someone the rendered odu-map and nothing else, and ask for two lists: the sections that can still take a 100G circuit, and the sections where nothing fits. Both come off the colours, green for the first and red for the second. Then check the answer against the containers on the branch rather than against the map, which is the last thing demo-odu does.

The check passes, and its summary line counts parents that fit, parents with no known figure and overfilled ones. Those counts come from the same slot table the map coloured itself with. A reader who names a green section the containers say is full has found a real disagreement. There is nowhere for one to hide: the map, the check and the generator all call one implementation of the capacity rule.

Two loadable scenarios for the regenerator​

Both are scenario input under demo/, and they are a before and after. They need two branches, because the point is the comparison: one branch keeps the refusal, and the other is the proposal a reviewer holds against it.

uv run invoke demo-regenerator

One command tells both halves. It makes oeo-refused, loads 06, asks for svc-mad-waw-400g and runs provisionable, which fails. Then it makes oeo-closed, loads 06 and 07, asks again, and runs the same check, which passes.

demo/06_mad_waw_16qam.yml, three regenerators and three refusals. Madrid to Warsaw is 2970 km over four sections, and no single wavelength closes it on any of the ten modes. The best is -0.021 dB at DP-QPSK 128GBd 400G. The file puts an OtnOduSwitch at each of the three sites on that route where one could go, oeo-par-01, oeo-fra-02 and oeo-prg-01, each holding the wavelength pair its split needs. All three splits are refused, reason budget: Paris returns -0.535 and -2.439 dB, Frankfurt -2.755, Prague -4.004. One run reports all three.

The two later splits show one figure each because the generator stops budgeting a chain at its first failing segment. A verdict is a conjunction, so the second half cannot rescue the first and computing it would only produce a number nobody may act on. Both halves of all four attempts are in the link budget table, measured segment by segment.

That refusal is the scenario and not a fault in the file. A route's verdict is a conjunction over its segments, so one half short of OSNR refuses the whole circuit however much margin the other half has.

This scenario ends in a blocked merge, and it is the only one that does. Nothing in demo/06_mad_waw_16qam.yml sets refusal_accepted, so a proposed change from oeo-refused goes red. The check the task runs on that branch says so:

svc-mad-waw-400g cannot be provisioned and was refused for budget: neither a
direct wavelength nor a chain serves this route. Direct:
oms-par-mad|oms-par-fra|oms-prg-fra|oms-prg-waw closes on no mode: best margin
-0.021 dB on DP-QPSK 128GBd 400G. Chain:
oc-ch070-mad-par|oeo-par-01|oc-ch070-par-waw does not close: segment 1 -0.535
dB, segment 2 -2.439 dB. It has not been accepted, so this branch does not
merge. Fix the network or the request, or set refusal_accepted on the service to
keep the refusal on the record

The service, the code and the detail are all in the one message, because a proposed change shows a reader this line and nothing else. The same run adds Judged 1 service(s). 1 refused and unaccepted, 0 refused with no readable code, 0 accepting a refusal that does not exist, 0 refused and signed for, 0 carrying an optical path.

Every other refusal in this guide is signed for. This one is deliberately not, so the gate is seen firing at least once. tests/unit/test_demo_scenarios.py::test_madrid_to_warsaw_signs_for_nothing_so_one_scenario_still_blocks fails if somebody adds the flag here, because a feature every scenario accepts its way past is a feature nobody watching the demo ever sees.

Two other checks are red on this branch for reasons that predate the gate. osnr_margin fails on a Paris to Madrid deficit that is on every branch including the default one. channel_collision fails because the three regenerator wavelength pairs this file loads overlap in spectrum on purpose. Neither is provisionable, and neither is this feature's doing.

demo/07_mad_waw_qpsk.yml, the mode that finishes the job. One more wavelength pair on the same Frankfurt split, at DP-QPSK 128GBd 400G, on oeo-fra-03. No new site, no new section, no edited margin. The service then provisions as two segments:

no direct wavelength serves
oms-par-mad|oms-par-fra|oms-prg-fra|oms-prg-waw, so it takes the chain
oc-ch073-mad-fra|oeo-fra-03|oc-ch073-fra-waw in 2 segments regenerated at
oeo-fra-03 at Frankfurt, 2970 km, segment 1 +2.745 dB, segment 2
+5.740 dB, 14558.963 us including the framing delay

Because 06 is loaded underneath, the same run still refuses all three 16QAM splits and says so. So one provisioning run tells the whole story: the fix for this route is a regenerator and a mode change, and three attempts at the regenerator alone were not enough. The link budget page has the table and what a chain costs.

This scenario refuses nothing, and it is the trap in the pair. The run logs three discarded 16QAM splits. A gate reading the log would block a branch on which the service ended up fine. It reads the node instead: svc-mad-waw-400g on oeo-closed is active, carries two path segments, and holds no reason code, no detail and refusal_accepted false. provisionable passes, and it is measured rather than argued. tests/unit/test_demo_scenarios.py::test_the_qpsk_fix_accepts_nothing_because_it_refuses_nothing holds the file to it, because a signature here would be one on a refusal that does not exist, which is its own error.

channel_collision is red on oeo-closed for the same reason it is red on oeo-refused: 06 is loaded underneath and its wavelength pairs overlap. That is the branch below, not the fix on top of it.

Why oeo-fra-03 and not oeo-fra-02. One device terminating all four Frankfurt wavelengths is a valid plant and it changes the answer, which was measured. With the 16QAM pair and the QPSK pair on one device, the generator takes the mixed cover and closes at +2.745 and +0.240 dB. Both halves close, so it is a real circuit, and it is a poorer demonstration. The second half survives on a quarter of a decibel, and a reader is left thinking the modulation mattered on one side only. A junction needs one device terminating both of its wavelengths, so keeping the QPSK pair on its own device makes that cover unavailable.

Two loadable scenarios for declared diversity​

These two share one branch, because the point is one run reporting a satisfied promise and a broken one side by side.

uv run invoke demo-diversity

It makes diversity-demo, loads both files, provisions all four members and then runs the check.

demo/08_diversity_mil_feeds.yml, a promise that holds. Milan's two northern feeds arrive through different trenches. Vienna to Milan takes oms-vie-mil, whose ducted span sits in cd-mil-northeast, and Geneva to Milan takes oms-gva-mil, in cd-mil-northwest. No conduit is on both routes, so the check passes. It passes because the routes are disjoint and not because nobody asked, which is the distinction the file exists to draw. A satisfied requirement and an absent requirement both look like a green check, and only one of them is worth anything.

demo/09_diversity_fra_feeds.yml, a promise that does not. Amsterdam to Frankfurt and Paris to Frankfurt are different cities on different sections, and both enter Frankfurt through cd-fra-north. The check fails, which is the expected result:

svc-fra-feed-ams-100g and svc-fra-feed-par-100g are both in diversity
group dg-frankfurt-feeds and their routes share cd-fra-north. One cut in
that duct takes both, so the diversity this group declares does not hold

With both files loaded the same run adds Checked 2 diversity group(s) over 4 routed service(s). 1 pair(s) share a conduit against a declaration and 0 member(s) have no route yet. Services declaring no group are not judged here. The Milan pair is judged and passes in the same breath as the Frankfurt pair is judged and fails. The last sentence is the check saying out loud what it did not look at.

Run the same check against the demo branch and it reports PASSED in silence. Those five services share four ducts between them, which the exposure report lists in scenario seven. None of them declared a group, so this check has nothing to say about any of it.

Provision before checking. A member with no route yet is reported as undetermined, and undetermined is not a pass. Running the check against one provisioned circuit proves nothing about the pair.

One loadable scenario for the missing monitor​

A single rather than a pair, because there is no second file worth reading beside it. The other scenarios on this page load something the network then refuses. This one loads a record an operator would sign off on, and the fault is that something else was never written. Deleting the file is the whole of the fix, so there is no "after" half to compare against.

uv run invoke demo-monitor-gap

demo/10_amplifier_without_monitor.yml, a stage nobody can measure. One OtnAmplifier, amp-ham-ber-11, written in the same fields as the 306 the dataset ships: every mandatory attribute set, every bound respected, no site because an amplifier hut is not a PoP. The load succeeds, and the success is the scenario. What is missing is an OtnAmplifierMonitor among its ports, and no constraint in schemas/ can refuse that. A schema constrains what is written, so it cannot notice a gap. ports peers the generic OtnGenericPort as well, and Infrahub cannot filter a relationship to a generic by the kind of its peers, so the schema has no way to say "at least one of these ports is a monitor" even in principle.

The check fails, with exactly one finding:

amp-ham-ber-11 is an OtnAmplifier and carries no OtnAmplifierMonitor, so nothing
can compare its configured gain against what it is delivering. The drift report
skips a stage with no monitor rather than reporting it

The message names what stops working and not only what is absent. A reader who meets "no monitor" has to work out whether that matters; a reader who meets this one already knows that the gain drift report is now quietly one stage short.

The drift report is a task of its own, and this branch is the interesting place to run it:

uv run invoke demo-drift --branch monitor-gap

It compares the gain each amplifier and Raman pump is configured for against the gain its monitor last reported, and lists the stages outside tolerance. The dataset seeds a droop, so the report always has something to say. What it cannot say anything about is amp-ham-ber-11: a stage with no monitor is a row the report never prints, which is why the check above has to be the thing that catches it.

The same run says what it counted:

Monitor coverage: 306/307 amplifiers, 9/9 Raman pumps, 59/59 transponders,
16/16 multiplexers, 42/42 ROADM degrees. Routers, patch panels and ODU switches
carry no monitor and are not judged here

Per kind rather than as one total, because the totals differ by two orders of magnitude and 306 covered amplifiers would hide nine uncovered Raman pumps inside a single percentage. The last sentence is the check naming the boundary of what it judged, so a reader is not left inferring it from silence. Run the same check against the default branch and the first figure reads 306/306 and the opening word changes to Monitor coverage complete.

59/59 transponders counts monitors, not light. Sixteen of those fifty-nine monitors sit on a transponder with no wavelength on either line port, and each reports loss of signal: received power at the floor of its range, and no OSNR, Q factor, dispersion or differential group delay. Before this dataset moved, every receiver reported the same healthy 25.1 dB whether or not anything was lit. This check has nothing to say about that either way. It asks whether a device carries a monitor, and a monitor honestly reporting darkness is covered. Concepts says why eight PoPs terminate nothing.

The new amplifier is in no section chain, and that is deliberate. oms-ham-ber has four spans and five amplifiers in each direction, because the budget engine pairs amplifier k with span k and puts a pre-amplifier after the last span. A sixth amplifier on a four-span section raises before a decibel is computed, so osnr_margin would turn red on a section this scenario has nothing to say about, and the branch would carry two failures with only one of them the point. The check does not need the relationship: it asks whether a device carries a monitor, and that question never goes through a section.

Negative result: no other check moves on this branch. channel_count_consistency still passes, because an amplifier lights no wavelength and no channel count changes. container_capacity, diversity and provisionable see no service and no container. A check that fired on an unrelated change would be a check nobody reads twice.

One loadable scenario for the multiplexer channel binding​

The second single, and the closest relative of the one above: both load records that every schema constraint accepts and that the model is nevertheless wrong about.

uv run invoke demo-mux-binding

demo/11_mux_channel_binding.yml, two client ports on the wrong number of channels. A multiplexer client port is one channel of the device, and it says which through one of two optional relationships: dwdm_channel to the dense 50 GHz grid, or cwdm_channel to the coarse plan. CH094 on mux-fra-01 binds neither. CH1531-2 on mux-ams-02 binds both, naming dense channel 94 and the coarse 1531 nm the device already lights.

The load succeeds for both, and the reason is a limit worth naming. The two plans are separate kinds, and Infrahub has no cross-relationship constraint: the schema can make one relationship mandatory or leave both optional, and it cannot say "exactly one of these two". Making either mandatory would leave the other plan's ports with no way to be written at all. So the schema takes what it can, which is the peer kind and the cardinality of each edge, and the check takes the rest.

The check fails, with exactly one finding per port:

mux-fra-01 carries client port CH094, which binds neither a dense channel nor a
coarse wavelength. The port is one channel of the multiplexer and nothing says
which, so no query can tell what light it passes and no service can be traced
through it
mux-ams-02 carries client port CH1531-2, which binds dense channel 94 and coarse
wavelength 1531 nm at the same time. One filter slot passes one wavelength, so
one of the two is wrong and the port's own record does not say which

Two findings and not three. The double binding names a coarse wavelength mux-ams-02 does list, so the check has nothing to add about an unlisted one, and each port draws exactly one complaint.

The same run says what it judged and what it did not:

16 multiplexer(s) examined, 96 client port(s) judged for binding exactly one
channel and 16 line port(s) skipped, because a line port carries every channel
the device lights and binds none by design. 14 of the devices list no coarse
wavelength, so their client ports are judged for the binding alone: the graph
holds no dense equivalent of cwdm_channels to compare a dense binding against

Negative result: half the question has no data behind it. A coarse multiplexer lists its wavelengths on cwdm_channels, so its ports and that list are two records that can be held against each other, and the check does. A dense one lists nothing, so a client port on the wrong dense channel is a fault this check cannot see. Fourteen of the sixteen units are in that position and the summary says so, because a count of what was skipped is the only thing that keeps silence from reading as a pass.

Negative result: no other check moves on this branch. A multiplexer port lights no wavelength on its own, carries no service and changes no channel count, so channel_count_consistency, container_capacity, diversity, provisionable and carrier_termination all read the branch exactly as they read the default one.

One loadable scenario for the attenuator range​

The third single, and the smallest question on this page: two numbers on one device, and the comparison between them that no schema constraint can make.

uv run invoke demo-attenuator-range

demo/12_attenuator_range.yml, a VOA asked for more than it has. voa-mil-01 is a 20.0 dB variable attenuator on the add stage at Milan, and the file restates it at 24.0 dB. Every other field is the figure the dataset already ships, so the setting is the only difference between this record and the racked device.

The load succeeds, and the split is the point. attenuation_mdb carries min_value: 0 and max_value: 30000, so the schema refuses a physically impossible figure at write time, from an object file, the API, the UI or a hand edit during a demo. 24,000 is inside that. What the schema cannot say is "not more than this device's own maximum", because that bound is a sibling attribute's value and Infrahub has no cross-attribute constraint. The schema takes the half it can and the check takes the one comparison left over.

The check fails, with exactly one finding:

voa-mil-01 is set to 24.000 dB and can produce 20.000 dB. The device will sit at
its stop instead, so the plant delivers 4.000 dB more power than this record
says it does and every budget computed from it is optimistic by that much

The same run says what it judged:

2 variable attenuator(s) judged against their own maximum, 0 of them dialled to
exactly that maximum and passing, because the bound is inclusive. Fixed pads are
not judged here and carry no range to judge: the absolute 0 to 30 dB limit is a
schema constraint on both kinds and is refused at write time

The bound is inclusive. A VOA sitting exactly at its maximum is at the end of its travel and is still a setting the device can hold, so equality passes. The demo dials past the stop rather than to it, because a scenario that turned on an off-by-one would be about the check's arithmetic rather than about the network.

Negative result: the fixed pads cannot be judged at all. The two pads at the ends of the CWDM tail have no max_attenuation_mdb, and that is the whole reason the attenuator is two kinds rather than one: a pad with a range is not a value to validate, it is a field that should not exist. The check reports that it did not judge them rather than counting them as passes, because a green result over something nobody looked at reads as evidence.

Negative result: no other check moves on this branch. An attenuator lights no wavelength, carries no service, holds no channel and has no ports, so osnr_margin, channel_count_consistency, container_capacity, diversity, provisionable, carrier_termination and mux_channel_binding all read the branch exactly as they read the default one. The one figure it would move is a link budget, and no optical path in this dataset runs through an attenuator yet.

One loadable scenario for the transceiver placement​

The fourth single, and the first of two about the pluggable optics the three router wavelengths run on.

uv run invoke demo-transceiver-placement

demo/13_transceiver_placement.yml, a module in a port with no cage. ZRP-BRU-01 is the OpenZR+ QSFP-DD in the Brussels router's line port. The file restates it into amp-ams-bru-08 OUT, the booster port at the Brussels end of the same section. That is the port an operator points at when asked where the wavelength leaves the site, and it is also a fixed optical interface on an EDFA with nothing to plug a module into.

The load succeeds, and the reason is the same limit the multiplexer scenario runs into from another direction. OtnTransceiver.port peers the OtnOpticalPort generic, and a relationship to a generic cannot be filtered by the kind of its peers. So the schema can offer the field or withhold it, and it cannot say "any of these three kinds and none of the other five". The port kind is the one thing this check is left holding.

The check fails, with exactly one finding:

QDD-400G-ZRP ZRP-BRU-01 is fitted in amp-ams-bru-08 OUT, a port of kind
OtnAmplifierPort. A pluggable optic goes in a line, a client or a router port,
and no other port kind has a cage to take one, so either this unit is somewhere
else or the port on its record is wrong

The same run says what it judged:

9 transceiver(s) examined, 6 of them fitted in a port and judged, 3 on a shelf
and not judged. An unfitted unit is a spare, an RMA or a decommissioned module,
and holding one is why the port relationship is optional. Two modules in one
port is not judged here: the schema refuses that write

A port holding two optics has no branch, because the server refuses to build one. OtnLinePort, OtnClientPort and OtnRouterPort each declare a cardinality-one transceiver on the same otn_optical_port__transceiver identifier OtnTransceiver.port uses, so both ends of the edge are cardinality one and the second module is rejected at write time:

Node 18d19871-0d7f-6758-306b-1188b66bb9eb has 2 peers for
otn_optical_port__transceiver, maximum of 1 allowed

That rule used to live in this check. uniqueness_constraints: [["port"]] was tried first and Infrahub accepts it only where the relationship is mandatory, and a mandatory port leaves a spare, an RMA and a decommissioned unit with nowhere to sit, which is three of the nine units here. The reverse edge buys the same refusal at no such cost, and it is declared on the three concrete kinds with a cage rather than on the OtnOpticalPort generic, which would have handed the field to the five kinds that hold no module.

A second check speaks on this branch and blocks nothing. Pulling the module out of rtr-bru-01 1/2/1 leaves oc-ch003-ams-bru with a pluggable at one end and integrated optics at the other, and transceiver_mode_support reports that as INFO:

oc-ch003-ams-bru is terminated by a pluggable at rtr-ams-01 1/2/1 and by
integrated optics at rtr-bru-01 1/2/1. The fitted end is judged against its own
part and the other end carries no part number to judge, which is a mixed
termination rather than a fault

Negative result: nothing here says the cage fits. A part carries a form_factor and a port does not, so "a QSFP-DD module in a QSFP-DD cage" is a comparison this model cannot make. The check refuses the port kind and says nothing about the cage, which is the honest half.

One loadable scenario for the transceiver mode support​

The fifth single, and the one where two records that match on every visible field are still the wrong pair.

uv run invoke demo-transceiver-mode

demo/14_transceiver_mode_support.yml, a 400ZR on an OpenZR+ wavelength. The OpenZR+ module comes out of the Amsterdam router's line port and the 400ZR spare off the shelf goes in. Both parts are QSFP-DD. Both are DP-16QAM. Both are 400G. Both are tunable. What differs is forward error correction, cFEC against oFEC, and therefore reach: 120 km against 1000 km, on a 220 km section.

The load succeeds, and no constraint could have refused it. supported_modes is a cardinality-many relationship on the part and the mode is a cardinality-one relationship on the carrier, two hops apart through a port. No attribute constraint reaches across two nodes and Infrahub has no cross-relationship constraint.

The check fails, with exactly one finding:

oc-ch003-ams-bru runs OpenZR+ 400G and rtr-ams-01 1/2/1 holds QDD-400G-ZR
ZR-SPARE-01, which supports 400ZR. The part does not list OpenZR+ 400G, so the
wavelength is planned on an optic that cannot produce it. Cage, constellation
and line rate say nothing here: the part number and the mode are the whole of
the difference

The same run says what it judged:

43 wavelength(s) examined, 3 judged against the parts fitted at their line ports
and 40 skipped for holding no pluggable at either end. A skipped wavelength is
terminated on integrated optics, which carry no part number and no mode list, so
this check's silence about them is not a verdict

Negative result: forty of the forty-three cannot be judged. A transponder lights its wavelength from a laser on the line card, so there is no part number and no mode list to compare anything against. Only the three router wavelengths carry a pluggable, and a run reporting no findings over forty-three skips would look exactly like a run that judged them all and found nothing. The split is printed for that reason.

Negative result: this check says nothing about whether the mode closes. Whether OpenZR+ 400G reaches over the plant is osnr_margin's question, and it does: +10.890 dB on oms-ams-bru. The 400ZR fitted here would fail on chromatic dispersion rather than on power, over its 2400 ps/nm tolerance by 1340 ps/nm with 8 dB of OSNR still in hand. That is the argument for oFEC and it belongs on the link budget page, not in this check.

One loadable scenario for the connector polish​

The sixth single, and the one where nothing about the object changed. What changed is the glass it faces.

uv run invoke demo-connector-polish

demo/15_connector_polish.yml, a blue jumper on pumped fibre. span-vie-mil-01 is the first span of the one section this network pumps, and its Vienna end is the only end of that section anyone can reach: spans 2 to 9 terminate in huts. This file re-patches that end. roadm-vie-01 DEG-MIL comes off and xpdr-vie-02 L2 goes on, a transponder patched straight onto the line past the ROADM. L2 is the one dark line port at Vienna, so nothing carrying traffic moved.

Its endface is UPC, as every line port in the dataset is. That is the right endface where a coloured line port normally sits, behind an add/drop stage inside a building, where return loss is a specification. On the far side of the ROADM it is a hazard: the nine pumps on this section put half a watt into the fibre and a flat endface sends about 50 dB of it back down the span.

The load succeeds, and no constraint could have refused it. polish is an attribute on the port and raman_pumps is a relationship on the span, two hops apart, and Infrahub has no cross-relationship constraint.

The check fails, with exactly one finding:

xpdr-vie-02 L2 terminates span-vie-mil-01, which is Raman-pumped, and its
endface is UPC. A pump puts half a watt into this fibre and a UPC endface
reflects that back down it, so this connector has to be APC before the pump is
turned up

The same run says what it judged:

133 span(s) examined, 9 of them Raman-pumped, 9 judged against their terminating
ports and 0 unjudgeable for naming none. An unjudgeable span is not a passing
one, and a run reporting no findings over a pumped section it could not reach
has seen nothing

That last zero is the figure to read. A span reaches its ports through OtnFiberSpan.terminating_ports, and a pumped span naming none is reported as unjudgeable rather than clean, because an empty relationship and two correct APC ends look identical from inside the check. The dataset generator populates all 133 spans so the count reads zero, and a run where it does not is a run whose green result means nothing.

Negative result: the relationship had to be added before the check could be written. No port kind had any edge to a fibre span or to an optical multiplex section in either direction. The nearest reachable ports were three hops away through the section and its ROADMs, and a ROADM carries one degree port per direction whose connected_to is null on every one of them, so the only thing naming the port facing a given span was the far site's shortname inside the port's name string. Parsing that is a naming convention doing a relationship's job, and the schema carries the edge instead.

Negative result: polish is judged on pumped glass and nowhere else. The 124 unpumped spans terminate on APC degree ports and the check says nothing about them, and it says nothing about the 126 line ports either, because none of them faces pumped fibre on the default branch. A flat endface is a normal, correct choice almost everywhere in this network.

The runbook​

About thirty minutes, one branch until step 12, in narrative order.

Setup, once. The object load takes a few minutes; start it before the audience arrives.

uv run invoke start
uv run invoke demo-setup

Every step below runs on the demo branch and none of them needs you to say so. Each one ends by naming the step that follows, and a step run out of order says which task to run first instead of failing inside the client. To watch the whole thing without typing between steps:

uv run invoke demo

1. The network, on one page. Two minutes, in the browser, before any command. Open OTN Sites, pick Frankfurt, open its Artifacts tab, and open network-map.

Fourteen PoPs in their real positions, 21 routes coloured by how much OSNR margin a 400G DP-16QAM carrier has on each. Point at the red route into Madrid, then at the chip on Frankfurt to Milan. Several of the findings the next twelve steps compute are already on this drawing, which is what makes it worth two minutes. The network map page has the rest of the legend.

odu-map is on the same Artifacts tab and is mostly grey here. Say what it is, leave it closed, and come back to it at step 12.

2. What is in the network, and what is left. Two minutes.

uv run invoke demo-capacity

Cover the 4,134,400 of 4,800,000 MHz first, then the eight-against-one gap between free spectrum and usable anchors. Then the sixteen empty sections, and the difference between a section's free spectrum and a route's.

3. Where the cheap parts reach. Two minutes. Run it before anybody asks.

uv run invoke demo-reach

Zero of twenty-one. Pause here: this is the step where the model returns an answer nobody wanted, computed from data rather than assumed.

4. Ask for a service and watch it choose. Four minutes.

uv run invoke demo-provision

Six candidate routes, Hamburg wins on fewest sections then margin. Read the discarded ones out loud: each one names the route, the mode, the margin and the reason it lost.

5. Provision the rest. One minute, and it is setup for what follows.

uv run invoke demo-provision-all

Two groom and two light, in that order, every time. svc-fra-mil-ai-400g and svc-ams-mil-ai-400g take tributary slots in wavelengths that are already up, oc-ch047-fra-mil and oc-ch002-ams-mil. Then svc-fra-gva-hpc-400g and svc-vie-mil-hpc-400g find no line container with room and each light channel 2, on oms-fra-gva and oms-vie-mil. Channel 2 and not channel 1, because a 79,600 MHz carrier centred on channel 1 falls off the bottom of the band. Say the grooming out loud: it is the ODU layer choosing not to buy spectrum, and it is why the cut in step 7 drops seven wavelengths and not eight.

6. Trace one of them. Two minutes.

uv run invoke demo-trace

Thirty-five hops, router to router. Point at the conduit column; it sets up step 8.

7. Cut the fiber. Three minutes. Leave room for questions on this one.

uv run invoke demo-impact

Seven wavelengths, 2.8 Tbps, one AI service, one customer, and six wavelengths the model holds with no customer behind them. Then the duct breakdown, and the fact that cd-fra-north also carries Paris to Frankfurt.

Then mistype it, deliberately:

uv run invoke demo-impact --section oms-ams-frs

It refuses. An impact tool that answers "no impact" to a typo is worse than one that answers nothing.

8. Who is not diverse. Two minutes.

uv run invoke demo-srlg

Frankfurt to Geneva and Amsterdam to Milan, no shared section, one shared duct.

9. The latency budgets. Two minutes.

uv run invoke demo-latency

Then the electronics column, and the sentence about there being no trade-off.

10. The one service that states its handover. Two minutes.

uv run invoke demo-infiniband

Frankfurt to Prague at 212 Gbps with client_signal: IB-HDR-4X, and the container comes back ODUflex. Drop that one line from the file and the same request provisions 400GBASE-FR4, with no error. The rate rule picks only from catalog rows marked auto_selectable, which the two InfiniBand rows are not.

212 Gbps and not 200: HDR signals at 212.5, which is 170 tributary slots, and a 200G wavelength offers 160. The request lands on DP-16QAM 64GBd 400G, whose ODUC4 offers 320, so the ODUflex takes 170 and leaves 150 free.

11. Congestion in two layers. Three minutes. Fill both layers of the Frankfurt to Milan corridor, then ask twice.

uv run invoke demo-refusal

The 400G is refused on slots: it needs a whole ODUC4 and the roomiest wavelength there has 240 of 320 free. The 100G then provisions on the same corridor without lighting anything. 532,400 MHz free in 29 blocks none of which reaches 44,400 says no new wavelength can be lit; it says nothing about room inside the ones already lit.

12. The other map, and one question off it. Three minutes, in the browser. This one wants its own branch, which the task makes:

uv run invoke demo-odu

Open odu-map on any PoP on odu-demo. Same coastline, same fourteen discs, different question. Ask the room for two lists off the colours alone: which sections can still take a 100G circuit, and which can take nothing. Green is the first, red is the second, and the panel's FITS column spells both out. The task finishes by checking the answer against the containers instead of against the map.

The two cannot disagree. The map, the check and the provisioning generator all call one implementation of the capacity rule. Finish on the grey: the sections carrying no wavelength at all, which the map says "not known" about rather than "available".

13. The branch. One minute. Open the proposed change and show the diff and the checks. Everything in the last thirty minutes happened on a branch, the default branch is untouched, and the checks decide whether any of it merges.

Clean up​

uv run invoke demo-clean

With no argument it deletes every branch a scenario task creates, naming each one as it goes and reporting how many of them it found. Nothing is removed without saying so, and --branch still takes a single name.

The default branch is back to the state it shipped in: the fourteen PoPs and their plant, the customer campus on the end of its coarse tail, no services, and the OSNR check still failing on Paris to Madrid. Every service in this guide was provisioned on a branch, which is the point.