Chapter 9: Designing Decisions: Atomic Questions, Fan-Out, and Composite Scoring

Learning Objectives

Pre-Quiz: The Five-Step Build Process

A change ticket says the window is "Sunday night after the batch run, before the 06:00 reporting jobs," while the CMDB stores a structured maintenance-window field for the same device. According to the five-step build process, how should each be handled?

Both should be sent to the model as questions, since timing is always uncertain.
The CMDB's structured window is compared in code (e.g., a datetime check); the free-text sentence must be interpreted as a judgment and sent as a question.
Both should be handled entirely in code, since maintenance windows are always well-defined.
The free-text sentence should be discarded and only the CMDB field used, since prose is unreliable.

Step 2 of the build process says to decompose the input state so a change-risk request includes the ticket's plans and a 40-line config diff, but not the switch's full 9,000-line running configuration. What is the main reason for leaving the full config out?

The full config would exceed the model's context window entirely.
Sending it once per request would dominate cost and latency, and a full config full of unrelated BGP stanzas can mislead a question that's actually about the diff.
The full running configuration contains no information relevant to any risk question.
TypeSafe requires all state to be under 40 lines by contract.

A NOC team wants to batch six risk questions into one request and then let a Python function combine the answers into a single approval decision. Which part of the five-step process does the combining step belong to, and why?

Step 3, because combining answers is itself an atomic question.
Step 4, because parallel questions must be combined by the model before they are returned.
Step 5, because deterministic rules or weighted sums belong in code, not in a model call.
None of the steps — combining logic is handled automatically by the API.

The Five-Step Build Process

Key Points

This chapter turns a messy operational judgment — "should we let this change go tonight?" — into a set of typed questions plus a few lines of Python you can defend to a change advisory board (CAB). TypeSafe documents this as five deliberately ordered steps.

StepWhat it means in the NOC
1. Use code when you canParse the diff, look up the device role in the CMDB, resolve the vPC peer, compute whether the requested time falls inside the approved window — no model call.
2. Decompose the input stateSend the ticket's summary, implementation plan, backout plan, test plan, and the diff — not the 9,000-line running config.
3. Use structure in questionsReplace "is this change risky?" with "does the diff modify BGP or OSPF configuration?"
4. Ask many questions togetherSix risk questions, an intent classifier, and speculative extras in a single request.
5. Combine outputs in coderisk = sum(weight * factor), then thresholds that pick auto-approve, CAB review, or reject.

Figure 9.1: The five-step build process

flowchart TD A["Step 1: Use code when you can"] --> B["Step 2: Decompose the input state"] B --> C["Step 3: Use structure in questions"] C --> D["Step 4: Ask many questions together"] D --> E["Step 5: Combine outputs in code"]

Step 1 is a discipline: anything a for loop, a regex, or a CMDB query can answer exactly should never become a question — deterministic work "is reliable and cheap." You would not ask a colleague whether 10.42.7.19 falls inside 10.42.0.0/16; you do the mask arithmetic. But when a window is described in free text ("Sunday night after the batch run"), no datetime object exists to compare — that sentence has to be interpreted, and interpretation is a judgment. The boundary between step 1 and step 3 is exactly this: structured data goes to code, prose goes to questions. Your service owns control flow; a System One request is a leaf in your program, not a driver of it.

Step 2 exists for two reasons. First, cost and latency: in a batched request the state dominates request size, so attaching a full config to every triage call is the single most expensive mistake you can make. Second, interpretation: paste an entire running config next to a 12-line diff and ask "does this change modify BGP?" and the model sees a config full of unrelated BGP stanzas. Think of state construction as building a SPAN session — mirror only the interfaces you care about.

def build_state(ticket: dict, diff_text: str, cmdb: dict) -> dict:
    """Assemble only the fields the risk questions actually need."""
    return {
        "change_number": ticket["number"],
        "short_description": ticket["short_description"],
        "implementation_plan": ticket["implementation_plan"],
        "backout_plan": ticket["backout_plan"],
        "test_plan": ticket["test_plan"],
        "requested_window_text": ticket["window_text"],
        "approved_window_text": cmdb["maintenance_window"],
        "device": {
            "hostname": cmdb["hostname"],
            "platform": cmdb["platform"],
            "role": cmdb["role"],
            "vpc_peer": cmdb["vpc_peer"],
            "supervisors": cmdb["supervisor_count"],
        },
        "config_diff": diff_text,
    }

Note what is absent: the full config, the comment thread, months of syslog. Note also what is present because code put it there — role, vpc_peer, and supervisors came from the CMDB in step 1, not from the model. Steps 4 and 5 are the payoff: many narrow questions about the same state go out in one request with no latency penalty, and the answers come back as independent typed values your code composes with rules or weighted sums. This auditability matters: when a composite score says 0.64 and the change goes to CAB, you can print the contributing factors and their weights — nobody has to trust a single opaque number.

Key Takeaway: The five-step process is an order of operations: do everything deterministic in code, send only the state the questions need, ask narrow atomic questions rather than broad ones, batch them into a single request, and combine the typed answers with rules or weights you control. The model contributes judgments; your code contributes control flow and arithmetic.
Post-Quiz: The Five-Step Build Process

A change ticket says the window is "Sunday night after the batch run, before the 06:00 reporting jobs," while the CMDB stores a structured maintenance-window field for the same device. According to the five-step build process, how should each be handled?

Both should be sent to the model as questions, since timing is always uncertain.
The CMDB's structured window is compared in code (e.g., a datetime check); the free-text sentence must be interpreted as a judgment and sent as a question.
Both should be handled entirely in code, since maintenance windows are always well-defined.
The free-text sentence should be discarded and only the CMDB field used, since prose is unreliable.

Step 2 of the build process says to decompose the input state so a change-risk request includes the ticket's plans and a 40-line config diff, but not the switch's full 9,000-line running configuration. What is the main reason for leaving the full config out?

The full config would exceed the model's context window entirely.
Sending it once per request would dominate cost and latency, and a full config full of unrelated BGP stanzas can mislead a question that's actually about the diff.
The full running configuration contains no information relevant to any risk question.
TypeSafe requires all state to be under 40 lines by contract.

A NOC team wants to batch six risk questions into one request and then let a Python function combine the answers into a single approval decision. Which part of the five-step process does the combining step belong to, and why?

Step 3, because combining answers is itself an atomic question.
Step 4, because parallel questions must be combined by the model before they are returned.
Step 5, because deterministic rules or weighted sums belong in code, not in a model call.
None of the steps — combining logic is handled automatically by the API.
Pre-Quiz: Atomic Questions

Why does the question "Is this change risky?" fail the atomic-question test that "Does this diff touch BGP?" passes?

It requires looking something up in the CMDB rather than the state provided.
It is a composite of several independent dimensions (blast radius, protocol impact, timing, rollback, testing) with no visible weighting, so different engineers — or the model — would answer it differently for different hidden reasons.
It is too short to give the model enough context.
It refers to a Score primitive instead of a Noul primitive.

Which of these candidate questions passes the "could a competent engineer answer this in about five seconds from the state alone" test?

"Will this change cause an outage?"
"Is this a good change to make?"
"Does the backout plan describe a concrete, executable rollback procedure?"
"How risky is this change overall?"

A CAB wants to add a seventh question — whether the implementation plan references a target release with a known field-notice issue — to an existing six-question risk model. Because the questions are independent, what should the team expect?

They need to re-validate all six existing questions, since adding a new one changes the request's structure.
The six existing answers will not change; the new question can be added, weighted, and tested without disturbing the others.
The new question will only be answered correctly if it is asked before the other six in the dictionary.
Adding a question requires a second API call, since each request supports a fixed number of questions.

Atomic Questions

Key Points

A real change advisory board's reasoning about a Cisco NX-OS change converges on recurring dimensions: impact scope, operational risk factors (rollback, testing), scheduling, and redundancy posture — plus, for Nexus platforms, vPC peer consistency and protocol convergence. Decomposed, "is this change risky?" becomes six answerable questions:

Question keyPrimitiveWhat it asks
core_deviceNoulTargets a core, spine, or aggregation switch rather than a single access leaf
routing_protocolNoulThe diff adds, removes, or modifies BGP or OSPF configuration
vpc_pairNoulThe device is one half of a vPC pair and the change affects vPC state
outside_windowNoulThe requested time falls outside the device's approved maintenance window
rollback_documentedNoulThe backout plan is a concrete, executable procedure, not boilerplate
lab_validationScoreHow thoroughly the change was validated before production

Figure 9.2: Decomposing "is this change risky?" into six atomic questions

graph TD Root["Is this change risky?"] --> Q1["core_device: targets core or aggregation switch"] Root --> Q2["routing_protocol: modifies BGP or OSPF"] Root --> Q3["vpc_pair: affects vPC peer consistency"] Root --> Q4["outside_window: outside approved maintenance window"] Root --> Q5["rollback_documented: concrete backout procedure"] Root --> Q6["lab_validation: how thoroughly tested"]

lab_validation is a Score because lab validation is genuinely ordinal — "syntax checked by a peer" sits between "nothing" and "validated on matching hardware at the target release," and collapsing that gradient into yes/no throws away the distinction a CAB spends the most time on. Notice also the positive phrasing of rollback_documented: write questions positively and invert them in code, next to the weight, where the polarity flip is visible — scattering negations through question text is how risk models quietly acquire sign errors.

The test to apply before shipping any question: could a competent engineer, handed only the state, answer this in about five seconds without looking anything up? "Does this diff touch BGP?" — yes, the engineer scans for router bgp. "Will this change cause an outage?" — no, that requires knowing traffic patterns and the future; it is a prediction, not a judgment. This test also catches questions that are really lookups in disguise — "does this device have dual supervisors?" is a CMDB field, not a judgment, and belongs in the state.

The property that makes the whole design work is independence: each question is scored on its own against the document, so its answer doesn't depend on what else is in the request. TypeSafe validated this empirically across five runs: answers matched whether asked alone or alongside a dozen others, regardless of document size. Operationally this means you can add a question without re-validating the others, unit-test one question at a time against labeled historical tickets, and reorder questions for readability without wondering whether answers shifted.

Key Takeaway: Replace one unanswerable composite judgment with a handful of questions that a knowledgeable engineer could each answer in seconds from the state alone. Because each question is scored independently against the state, you can add, test, and reorder questions without disturbing the others — which is what makes a risk model maintainable rather than a prompt you are afraid to touch.
Post-Quiz: Atomic Questions

Why does the question "Is this change risky?" fail the atomic-question test that "Does this diff touch BGP?" passes?

It requires looking something up in the CMDB rather than the state provided.
It is a composite of several independent dimensions (blast radius, protocol impact, timing, rollback, testing) with no visible weighting, so different engineers — or the model — would answer it differently for different hidden reasons.
It is too short to give the model enough context.
It refers to a Score primitive instead of a Noul primitive.

Which of these candidate questions passes the "could a competent engineer answer this in about five seconds from the state alone" test?

"Will this change cause an outage?"
"Is this a good change to make?"
"Does the backout plan describe a concrete, executable rollback procedure?"
"How risky is this change overall?"

A CAB wants to add a seventh question — whether the implementation plan references a target release with a known field-notice issue — to an existing six-question risk model. Because the questions are independent, what should the team expect?

They need to re-validate all six existing questions, since adding a new one changes the request's structure.
The six existing answers will not change; the new question can be added, weighted, and tested without disturbing the others.
The new question will only be answered correctly if it is asked before the other six in the dictionary.
Adding a question requires a second API call, since each request supports a fixed number of questions.
Pre-Quiz: Speculative Fan-Out

A team currently issues six separate System One calls for six risk questions on the same change ticket, resending the full state each time. Why does batching them into one call save so much, per the documented GDPR case study (12.2x cheaper, 10.0x faster for 13 questions)?

Because each question is answered by a smaller, cheaper model when batched.
Because the state is sent and counted once, and each additional question adds only the small number of tokens in its own instructions, rather than the whole state being resent per question.
Because the API caches answers from prior questions and reuses them for later ones.
Because batched requests skip the confidence-scoring step entirely.

Before knowing whether a change ticket is an OS upgrade, a VLAN edit, or a SPAN session, the NOC service asks a change_intent classifier plus issu_claimed and orphan_ports_addressed questions all in the same request, even though the latter two only matter for OS upgrades. What pattern is this, and what does it save?

Composite scoring — it saves by combining the answers with weights.
Speculative fan-out — it saves a second round trip, since if the ticket turns out to be an OS upgrade, the answers are already there at the cost of roughly the extra questions' tokens.
Intent routing — it saves by sending the request to a specialist LLM instead of System One.
Decomposition — it saves by shrinking the state sent to the model.

A risk-scoring service classifies change_intent as os_upgrade and then needs to attach a 4,000-line release-notes document to check for known compatibility issues on the target platform. Should this be handled as more speculative questions in the original request, or a second request?

More speculative questions in the original request, since fan-out removes all round trips.
A second request, because this is state that wasn't in the original request and would be too expensive to attach speculatively to every ticket.
More speculative questions, since the release notes are a question, not state.
Neither — release-note compatibility should never be checked by System One.

Speculative Fan-Out

Key Points

Speculative fan-out is one of the core System One patterns, alongside composite scoring and intent routing. Because all questions in a request are evaluated simultaneously, adding more questions typically adds no latency — two questions and fifteen questions over the same state run in roughly the same wall-clock time. The cost savings come from token accounting: the state is sent once and counted once, and each additional question adds only a sentence or two of instructions. Translated to NOC triage: a ticket plus diff might be 1,200 tokens; six risk questions add perhaps 150 tokens between them. Asking them separately means resending 7,200 tokens of state for identical answers.

Figure 9.3: Speculative fan-out versus serial round trips

flowchart LR subgraph Serial["Serial round trips"] direction LR S1["State sent"] --> S2["Question 1"] S2 --> S3["State sent again"] S3 --> S4["Question 2"] S4 --> S5["...repeat per question"] end subgraph FanOut["Speculative fan-out"] direction LR F1["State sent once"] --> F2["All questions in parallel"] F2 --> F3["Code reads relevant answers"] end

Visual animation — coming soon

The principle: speculative questions are ignored when irrelevant and save a round trip when they are not. Before knowing whether a ticket is an OS upgrade, a VLAN edit, or a SPAN session, the fan-out request asks everything at once:

FANOUT_QUESTIONS = {
    **RISK_QUESTIONS,                # Always used.

    "change_intent": CHANGE_INTENT,  # Classifier

    # Speculative: only read when change_intent == "os_upgrade".
    "issu_claimed": Noul(instructions=(
        "The implementation plan states the upgrade will be performed "
        "nondisruptively using ISSU rather than a traditional reload."
    )),
    "orphan_ports_addressed": Noul(instructions=(
        "The plan acknowledges devices connected to orphan ports and "
        "describes how their connectivity is handled during the reload."
    )),

    # Speculative: only read when change_intent == "emergency_patch".
    "cve_referenced": Noul(
        instructions="The ticket references a specific CVE or field notice.",
    ),
}

If the ticket turns out to be a SPAN session, the two ISSU-related answers come back unused at a cost of roughly sixty tokens. If it is an upgrade, the answers are already there — no second request, no second latency hit. The routing code stays clean because the conditionals live in one place:

def upgrade_flags(response, intent: str) -> dict:
    """Read speculative answers only when the intent makes them meaningful."""
    a = response.answers
    if intent == "os_upgrade":
        return {
            "issu_claimed": a["issu_claimed"].noul,
            "orphan_ports_addressed": a["orphan_ports_addressed"].noul,
        }
    if intent == "emergency_patch":
        return {"cve_referenced": a["cve_referenced"].noul}
    return {}

Fan-out removes round trips caused by question dependencies, not state dependencies. A second request is genuinely warranted when the first answer changes what data you must gather (e.g., pulling release notes or a compatibility matrix once os_upgrade is known), when the speculative state would be expensive for everyone (don't attach a 4,000-line document to every ticket on the chance one in twenty needs it), or when a human resubmits changed state after a request for a real backout plan.

Key Takeaway: Because questions are evaluated in parallel and the state is sent once, extra questions cost a few tokens and essentially no latency — batching thirteen questions instead of issuing thirteen calls measured 12.2x cheaper and 10.0x faster. Ask speculative questions up front and let code ignore the irrelevant ones; reserve a second request for the case where the first answer forces you to gather genuinely new state.
Post-Quiz: Speculative Fan-Out

A team currently issues six separate System One calls for six risk questions on the same change ticket, resending the full state each time. Why does batching them into one call save so much, per the documented GDPR case study (12.2x cheaper, 10.0x faster for 13 questions)?

Because each question is answered by a smaller, cheaper model when batched.
Because the state is sent and counted once, and each additional question adds only the small number of tokens in its own instructions, rather than the whole state being resent per question.
Because the API caches answers from prior questions and reuses them for later ones.
Because batched requests skip the confidence-scoring step entirely.

Before knowing whether a change ticket is an OS upgrade, a VLAN edit, or a SPAN session, the NOC service asks a change_intent classifier plus issu_claimed and orphan_ports_addressed questions all in the same request, even though the latter two only matter for OS upgrades. What pattern is this, and what does it save?

Composite scoring — it saves by combining the answers with weights.
Speculative fan-out — it saves a second round trip, since if the ticket turns out to be an OS upgrade, the answers are already there at the cost of roughly the extra questions' tokens.
Intent routing — it saves by sending the request to a specialist LLM instead of System One.
Decomposition — it saves by shrinking the state sent to the model.

A risk-scoring service classifies change_intent as os_upgrade and then needs to attach a 4,000-line release-notes document to check for known compatibility issues on the target platform. Should this be handled as more speculative questions in the original request, or a second request?

More speculative questions in the original request, since fan-out removes all round trips.
A second request, because this is state that wasn't in the original request and would be too expensive to attach speculatively to every ticket.
More speculative questions, since the release notes are a question, not state.
Neither — release-note compatibility should never be checked by System One.
Pre-Quiz: Composite Scoring and Intent Routing

Why does the composite risk formula normalize every factor to a 0–1 scale and require the weights to sum to 1.0, instead of using arbitrary scales and weights?

So the raw output is always exactly the average of all six factors.
So a threshold like 0.7 means the same thing across every factor, and results stay comparable across different changes and over time.
Because the System One API only accepts weights between 0 and 1.
So that a single factor can never influence the score by more than 50%.

In the documented route_ticket() reference implementation, the confidence check runs before any check of intent.choice. Why does the confidence gate come first?

Because confidence is cheaper to compute than the intent classification itself.
Because a low-confidence answer is not the same fact as a low-risk answer — an uncertain classification must escalate to a human before any automated branch acts on it.
Because ITIL 4 requires confidence to be logged before intent, for audit purposes.
Because checking confidence first reduces the number of tokens used by the request.

A ticket reads "Add a SPAN session to mirror Po10 to Eth1/47." Per the chapter's intent-routing table, which handler should this route to, and why?

queue_for_cab(sme_required=True), because any config change on a switch warrants full CAB review.
score_and_route(), because SPAN sessions can affect vPC peer consistency.
handle_standard_change(), because monitoring-only changes like SPAN sessions are deterministic, low-risk, auto-approved, and map to ITIL 4's "standard" change type.
route_to_human_agent(), because SPAN session tickets are always low confidence.

Composite Scoring and Intent Routing

Key Points

Composite scoring combines several independently scored dimensions into a single score: break the judgment into independent dimensions, score each separately, and combine them with weights you control in code. A weighted sum in its simplest form:

risk = 0.4 * blast_radius + 0.4 * protocol_impact + 0.2 * (1 - rollback_quality)

Every input is on a 0–1 scale, so the output is too, and a threshold like 0.7 means the same thing across every factor. The third term is inverted — rollback_quality is good, and (1 - x) converts it into a risk contribution. Network engineers already have a mental model for this: QoS. You classify traffic into independent classes, then a policy-map decides how much of the interface each class gets; nobody asks the classifier to also set the bandwidth allocation. A composite score is a policy-map over judgments.

The value of composite scoring collapses if weights are scattered through the code. Keep them in one dictionary, at module scope, with an assertion:

# The only place risk weights are defined. Changes here go through CAB review.
RISK_WEIGHTS = {
    "core_device":      0.20,  # blast radius
    "routing_protocol": 0.15,  # BGP/OSPF convergence exposure
    "vpc_pair":         0.20,  # peer consistency, orphan-port impact
    "outside_window":   0.15,  # scheduling risk
    "no_rollback":      0.15,  # inverted from rollback_documented
    "untested":         0.15,  # inverted from lab_validation
}
assert abs(sum(RISK_WEIGHTS.values()) - 1.0) < 1e-9

THRESHOLDS = {
    "auto_approve_risk_max":        0.25,
    "reject_risk_min":              0.70,
    "auto_approve_confidence_min":  0.85,
    "human_confidence_floor":       0.50,
}

This block is short enough to read in a CAB meeting and diffs cleanly in a pull request. These numbers are a starting point, not a discovered truth — weights encode your organization's risk appetite, and the only defensible way to set them is to replay historical changes and check that the ones that went badly score high.

Intent routing classifies a request and routes it to the appropriate handler, using System One as a fast classifier for two dimensions: intent (request type) and complexity (resolution difficulty). For change tickets, this maps onto ITIL 4's change types:

Ticket intentHandlerITIL change type
monitoring_only / interface_edithandle_standard_change() — auto-approved runbookStandard
vlan_change / routing_policyscore_and_route() — composite risk scorerNormal
os_upgradequeue_for_cab(sme_required=True)Normal, high risk
emergency_patchhandle_emergency_change()Emergency
low confidencequeue_for_change_manager() — humanDetermined by the human

The last row is the critical design principle: confidence-aware routing. Low confidence triggers escalation, preventing costly automated errors on uncertain classifications. The reference implementation checks confidence before it checks the answer:

def route_ticket(ticket_id, response):
    intent = response.answers["intent"]
    complexity = response.answers["complexity"]

    # Low confidence routes to human agents
    if intent.confidence < 0.5:
        return route_to_human_agent(ticket_id)

    # Different intents trigger different handlers
    if intent.choice == "order_status":
        handle_order_status(ticket_id)
    elif intent.choice in ["product_question", "return_exchange"]:
        handle_with_llm(ticket_id, SPECIALIST_CONTEXT)
    elif intent.choice == "complaint":
        if complexity.score > 1 or complexity.confidence < 0.5:
            route_to_human_agent(ticket_id)

The confidence gate comes first and short-circuits everything; intents dispatch to different kinds of handlers, not just different branches of one; and complexity modulates automated-versus-human handling within a single intent.

Figure 9.4: Intent routing classifies the ticket and dispatches to a handler

flowchart LR A["Change ticket"] --> B["Intent classifier"] B -->|"monitoring_only or interface_edit"| C["Deterministic runbook: auto-approved"] B -->|"vlan_change or routing_policy"| D["Composite risk scorer"] B -->|"os_upgrade"| E["Full CAB with SME on standby"] B -->|"emergency_patch"| F["Expedited approval"] B -->|"low confidence"| G["Human change manager"]
Key Takeaway: Composite scoring turns independently scored 0–1 dimensions into one number using weights that live in a single reviewable dictionary, so you can recalibrate without re-running evaluations. Intent routing puts a fast classifier in front of the pipeline so that standard changes take a deterministic path, normal changes get scored, and anything the classifier is unsure about goes to a human before any automation acts.
Post-Quiz: Composite Scoring and Intent Routing

Why does the composite risk formula normalize every factor to a 0–1 scale and require the weights to sum to 1.0, instead of using arbitrary scales and weights?

So the raw output is always exactly the average of all six factors.
So a threshold like 0.7 means the same thing across every factor, and results stay comparable across different changes and over time.
Because the System One API only accepts weights between 0 and 1.
So that a single factor can never influence the score by more than 50%.

In the documented route_ticket() reference implementation, the confidence check runs before any check of intent.choice. Why does the confidence gate come first?

Because confidence is cheaper to compute than the intent classification itself.
Because a low-confidence answer is not the same fact as a low-risk answer — an uncertain classification must escalate to a human before any automated branch acts on it.
Because ITIL 4 requires confidence to be logged before intent, for audit purposes.
Because checking confidence first reduces the number of tokens used by the request.

A ticket reads "Add a SPAN session to mirror Po10 to Eth1/47." Per the chapter's intent-routing table, which handler should this route to, and why?

queue_for_cab(sme_required=True), because any config change on a switch warrants full CAB review.
score_and_route(), because SPAN sessions can affect vPC peer consistency.
handle_standard_change(), because monitoring-only changes like SPAN sessions are deterministic, low-risk, auto-approved, and map to ITIL 4's "standard" change type.
route_to_human_agent(), because SPAN session tickets are always low confidence.
Pre-Quiz: Worked Example: Change-Risk Score for a Cisco NX-OS Ticket

For CHG0041992, the test plan states the change was "validated on a lab Nexus 93180YC running 9.3(8)," while production is a Nexus 9372PX running 9.3(10). Why was lab_validation modeled as a Score rather than a Noul, and what did the model return?

As a Noul, because lab validation is a strict yes/no fact; it returned 0.71.
As a Score across four ordinal levels, because the testing described is genuinely between two levels; it returned a score of 1.2 with confidence 0.71.
As a Score, because Scores are required whenever a question references a Cisco platform.
As a Noul, and it returned 0.93, the same as core_device.

CHG0041992's composite risk came out to 0.641, and its decision confidence came out to 0.71. Why is 0.71 — not the average of all six certainties — used as the decision confidence?

0.71 happens to be the average of the five Noul certainties and the Score confidence.
decision_confidence() takes the minimum across all derived certainties, treating the flow as only as certain as its weakest input — here, the lab_validation confidence of 0.71.
0.71 is a fixed threshold defined in THRESHOLDS, not a computed value.
The composite risk score is divided by the number of factors to produce the confidence.

CHG0041992 lands at composite risk 0.641 and decision confidence 0.71, against thresholds auto_approve_risk_max=0.25, reject_risk_min=0.70, auto_approve_confidence_min=0.85, and human_confidence_floor=0.50. What outcome does route_change() produce, and why?

auto_approve, because 0.71 confidence is high enough and the change has a documented rollback plan.
reject, because 0.641 is close to the reject threshold of 0.70.
cab_review, because confidence (0.71) clears the human floor (0.50) but risk (0.641) is above the auto-approve ceiling (0.25) and below the reject line (0.70), and the reject gate additionally requires no_rollback > 0.5, which isn't the case here.
cab_review, because the confidence of 0.71 is below the human_confidence_floor of 0.50.

Worked Example: Change-Risk Score for a Cisco NX-OS Ticket

Key Points

The NOC triage service picks up a ServiceNow change request for a Nexus aggregation switch. Code has already resolved the device in the CMDB and rendered the intended configuration delta.

CHG0041992  Add eBGP peering to new transit provider on nexus-agg-01
Requested window: Sunday 02:00-04:00, after the batch run
Backout plan: Reload with previous image from bootflash if required.
Test plan: Config validated on a lab Nexus 93180YC running 9.3(8).

CMDB facts assembled by code: role=aggregation, platform=N9K-C9372PX, vpc_peer=nexus-agg-02, supervisor_count=2, approved window Sunday 01:00–05:00. Six questions run in parallel over the assembled state:

from typesafe_sdk import Noul, Score, TypeSafeClient

client = TypeSafeClient()  # defaults to jev-latest

LAB_LEVELS = [
    "No lab or staging validation is mentioned",
    "Configuration was syntax-checked or peer-reviewed only",
    "Tested in a lab on different hardware or a different software release",
    "Validated on lab hardware matching the production model and target release",
]

RISK_QUESTIONS = {
    "core_device": Noul(instructions="..."),
    "routing_protocol": Noul(instructions="..."),
    "vpc_pair": Noul(instructions="..."),
    "outside_window": Noul(instructions="..."),
    "rollback_documented": Noul(instructions="..."),
    "lab_validation": Score(instructions="How thoroughly validated", criteria=LAB_LEVELS),
}

response = client.system_one(state=state, questions=RISK_QUESTIONS)

One call, six judgments. Illustratively, the returned shape for this ticket:

{
  "model": "jev-latest",
  "answers": {
    "core_device":         { "noul": 0.93 },
    "routing_protocol":    { "noul": 0.98 },
    "vpc_pair":            { "noul": 0.96 },
    "outside_window":      { "noul": 0.08 },
    "rollback_documented": { "noul": 0.91 },
    "lab_validation":      { "score": 1.2, "confidence": 0.71 }
  }
}

A Noul answer is the probability of a yes on a 0–1 scale; Score answers carry an explicit confidence. The lab_validation result of 1.2 sits mostly on level 1 with some weight on level 2: the model recognized "validated on a lab Nexus 93180YC running 9.3(8)" as a different platform and release from the production N9K-C9372PX on 9.3(10). Confidence of 0.71 reflects genuine ambiguity — more than a syntax check, less than matching-hardware validation.

def risk_factors(response) -> dict[str, float]:
    """Turn typed answers into 0-1 risk contributions. Inversions happen HERE."""
    a = response.answers
    return {
        "core_device":      a["core_device"].noul,
        "routing_protocol": a["routing_protocol"].noul,
        "vpc_pair":         a["vpc_pair"].noul,
        "outside_window":   a["outside_window"].noul,
        "no_rollback":      1.0 - a["rollback_documented"].noul,
        "untested":         1.0 - a["lab_validation"].score / 3,
    }

def composite_risk(factors: dict[str, float]) -> float:
    return sum(RISK_WEIGHTS[key] * value for key, value in factors.items())

def decision_confidence(response) -> float:
    """Weakest link: the flow is only as certain as its least certain input."""
    certainties = [noul_certainty(response.answers[k].noul) for k in NOUL_KEYS]
    certainties.append(response.answers["lab_validation"].confidence)
    return min(certainties)

Running CHG0041992 through it:

FactorValueWeightContribution
core_device0.930.200.186
routing_protocol0.980.150.147
vpc_pair0.960.200.192
outside_window0.080.150.012
no_rollback (1−0.91)0.090.150.014
untested (1−1.2/3)0.600.150.090
Composite risk1.000.641

Decision confidence is the minimum of the five derived Noul certainties (0.86–0.96) and the lab_validation confidence of 0.71 — so 0.71, set by the lab question. That is informative in itself: the weakest part of this assessment is not whether BGP is involved, it is how well the change was tested — exactly what a CAB should press on.

def route_change(response) -> dict:
    factors = risk_factors(response)
    risk = composite_risk(factors)
    conf = decision_confidence(response)

    # Gate 1: confidence before content.
    if conf < THRESHOLDS["human_confidence_floor"]:
        return decide("cab_review", "Confidence below floor", risk, conf, factors)

    # Gate 2: high risk with no real backout plan is not a CAB debate.
    if risk >= THRESHOLDS["reject_risk_min"] and factors["no_rollback"] > 0.5:
        return decide("reject", "High risk, no backout plan", risk, conf, factors)

    # Gate 3: auto-approve requires BOTH low risk and high confidence.
    if (risk <= THRESHOLDS["auto_approve_risk_max"]
            and conf >= THRESHOLDS["auto_approve_confidence_min"]):
        return decide("auto_approve", "Low risk, high confidence", risk, conf, factors)

    return decide("cab_review", "Risk above the auto-approve band", risk, conf, factors)

CHG0041992 lands at risk 0.641 with confidence 0.71: above the auto-approve band, below the reject line, comfortably above the confidence floor — CAB review, with the six factors written into the ticket's work notes so the board opens the meeting already knowing that blast radius, vPC exposure, and thin lab validation drove the number. Contrast a monitoring-only SPAN-session ticket, well inside the window with a one-line backout and a runbook executed forty times: composite risk lands around 0.05 with confidence 0.88 — auto-approve, matching ITIL 4's treatment of a monitoring SPAN session as a pre-approved standard change.

The confidence gate runs first, before any risk comparison, because a low-confidence 0.1 and a high-confidence 0.1 are not the same fact — one is "this is safe," the other is "I could not tell." Collapsing them is how confidence-aware systems quietly become confidence-blind ones. Auto-approve requires both low risk and high confidence, while reject requires high risk and a missing rollback: the gates are asymmetric on purpose, because wrongly auto-approving a fabric change costs far more than sending a routine one to a board that meets twice a week. This flow does not execute the change or replace the board — it produces a defensible recommendation with its reasoning attached, letting the CAB spend its meeting on the changes that need argument.

Figure 9.5: Confidence-gated approval flow

flowchart TD A["Compute composite risk and decision confidence"] --> B{"Confidence below human floor?"} B -->|Yes| C["CAB review: low confidence"] B -->|No| D{"High risk and no rollback?"} D -->|Yes| E["Reject"] D -->|No| F{"Low risk and high confidence?"} F -->|Yes| G["Auto-approve"] F -->|No| H["CAB review: above auto-approve band"]

Visual animation — coming soon

Key Takeaway: The complete pattern is one system_one call carrying six atomic questions over a decomposed state, a weighted sum whose constants live in one dictionary, and a router that checks confidence before it checks risk. Every number in the final decision traces back to a named factor and a reviewable weight, which is what makes the output something a change advisory board can audit rather than something it has to trust.
Post-Quiz: Worked Example: Change-Risk Score for a Cisco NX-OS Ticket

For CHG0041992, the test plan states the change was "validated on a lab Nexus 93180YC running 9.3(8)," while production is a Nexus 9372PX running 9.3(10). Why was lab_validation modeled as a Score rather than a Noul, and what did the model return?

As a Noul, because lab validation is a strict yes/no fact; it returned 0.71.
As a Score across four ordinal levels, because the testing described is genuinely between two levels; it returned a score of 1.2 with confidence 0.71.
As a Score, because Scores are required whenever a question references a Cisco platform.
As a Noul, and it returned 0.93, the same as core_device.

CHG0041992's composite risk came out to 0.641, and its decision confidence came out to 0.71. Why is 0.71 — not the average of all six certainties — used as the decision confidence?

0.71 happens to be the average of the five Noul certainties and the Score confidence.
decision_confidence() takes the minimum across all derived certainties, treating the flow as only as certain as its weakest input — here, the lab_validation confidence of 0.71.
0.71 is a fixed threshold defined in THRESHOLDS, not a computed value.
The composite risk score is divided by the number of factors to produce the confidence.

CHG0041992 lands at composite risk 0.641 and decision confidence 0.71, against thresholds auto_approve_risk_max=0.25, reject_risk_min=0.70, auto_approve_confidence_min=0.85, and human_confidence_floor=0.50. What outcome does route_change() produce, and why?

auto_approve, because 0.71 confidence is high enough and the change has a documented rollback plan.
reject, because 0.641 is close to the reject threshold of 0.70.
cab_review, because confidence (0.71) clears the human floor (0.50) but risk (0.641) is above the auto-approve ceiling (0.25) and below the reject line (0.70), and the reject gate additionally requires no_rollback > 0.5, which isn't the case here.
cab_review, because the confidence of 0.71 is below the human_confidence_floor of 0.50.

Key Terms

TermDefinition
atomic questionA question narrow and explicit enough to be answered on its own from the supplied state, without depending on any other question's answer.
decompositionBreaking a complex judgment (or an oversized input state) into smaller independent parts — separate dimensions to score, and only the context each one needs.
independenceThe property that each question is scored on its own against the state, so its answer does not change based on what other questions are in the request or the order they appear in.
speculative fan-outSending many questions in one call, including conditional ones you may not use, and letting code decide afterward what is relevant.
composite scoringCombining several independently scored dimensions into a single number using weights controlled in code, rather than asking the model for the combined judgment directly.
weighted sumThe arithmetic behind composite scoring: each dimension normalized to 0–1, multiplied by a weight, and added, with the weights summing to 1.0.
intent routingUsing System One as a fast classifier to determine which handler to invoke — deterministic logic, a specialist LLM, or a human — instead of sending every request down the same expensive path.
control flowThe order, branching, and looping of your program. Control flow stays in your code; the model contributes bounded judgments at the leaves.
confidence-aware routingChecking an answer's confidence before acting on its value, so that low-confidence results escalate to a human instead of triggering automated action.
normalizationRescaling a raw answer (for example, an expected Score across four levels) onto a 0–1 range so it can be combined with other dimensions under a common set of weights.
ITIL 4 change typesStandard (pre-approved, low-risk, repeatable), normal (requires risk assessment and formal approval), and emergency (expedited approval with mandatory post-implementation review).
blast radiusThe number of devices, users, services, and downstream dependencies affected if a change goes wrong.
ISSUIn-Service Software Upgrade: a nondisruptive NX-OS upgrade method that keeps the data plane forwarding traffic, available only when conditions such as dual supervisors and an operational vPC peer link are met.

Your Progress

Answer Explanations