packet-tracer-skill 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +424 -42
  2. package/README.md +535 -250
  3. package/SKILL.md +337 -262
  4. package/bin/packet-tracer-skill.js +29 -2
  5. package/docs/automation-controller-proof.md +35 -0
  6. package/docs/curated-donor-registry.md +11 -0
  7. package/docs/generate-ready-pilot-design.md +30 -0
  8. package/docs/github-launch-ops-0.2.3.md +37 -0
  9. package/docs/github-metadata.md +6 -4
  10. package/docs/hero-demo-plan.md +1 -1
  11. package/docs/home-iot-donor-proof.md +4 -4
  12. package/docs/industrial-programming-proof.md +48 -0
  13. package/docs/ipv4-routing-management-proof.md +37 -0
  14. package/docs/l2-resiliency-bgp-proof.md +60 -0
  15. package/docs/l2-security-qos-proof.md +59 -0
  16. package/docs/packet-tracer-feature-gap-atlas.md +174 -17
  17. package/docs/post-launch-follow-up.md +9 -5
  18. package/docs/proof-readiness-dashboard.md +69 -0
  19. package/docs/publish-preview-roadmap.md +6 -5
  20. package/docs/release-checklist.md +27 -13
  21. package/docs/release-notes-0.2.2.md +1 -1
  22. package/docs/release-notes-0.2.3.md +59 -0
  23. package/docs/release-notes-0.2.4.md +20 -0
  24. package/docs/runtime-truth.md +33 -8
  25. package/docs/security-edge-deepening-proof.md +65 -0
  26. package/docs/voice-collaboration-proof.md +38 -0
  27. package/docs/wan-security-donor-proof.md +20 -3
  28. package/examples/README.md +98 -69
  29. package/examples/complex_campus_master_edit_v4.inventory.json +12 -2
  30. package/examples/gallery.md +94 -6
  31. package/examples/home_iot_cli_edit_v1.inventory.json +11 -2
  32. package/examples/index.json +932 -4
  33. package/examples/local-sample-evidence.json +24 -0
  34. package/examples/proof-cards.json +117 -0
  35. package/examples/service_heavy_cli_edit_v1.inventory.json +11 -2
  36. package/package.json +60 -44
  37. package/pytest.ini +9 -0
  38. package/references/packettracer-feature-atlas.json +67 -17
  39. package/references/packettracer-sample-catalog.json +45287 -4525
  40. package/references/packettracer-sample-catalog.md +599 -259
  41. package/references/proof-readiness-candidates.json +352 -0
  42. package/scripts/build_examples_index.py +228 -35
  43. package/scripts/build_sample_catalog.py +24 -44
  44. package/scripts/corpus_runner.py +430 -0
  45. package/scripts/coverage_matrix.py +1842 -1319
  46. package/scripts/donor_cache.py +354 -0
  47. package/scripts/donor_diagnostics.py +3 -1
  48. package/scripts/feature_atlas.py +65 -1
  49. package/scripts/generate_pkt.py +8762 -4070
  50. package/scripts/intent_parser.py +2242 -1138
  51. package/scripts/local_donors.py +340 -0
  52. package/scripts/packet_tracer_env.py +846 -391
  53. package/scripts/pkt_annotate.py +218 -0
  54. package/scripts/pkt_codec.py +420 -181
  55. package/scripts/pkt_editor.py +2405 -1226
  56. package/scripts/pkt_transformer.py +1072 -727
  57. package/scripts/pkt_verify.py +461 -0
  58. package/scripts/remote_search.py +197 -21
  59. package/scripts/runtime_doctor.py +80 -29
  60. package/scripts/sample_catalog.py +1372 -1195
  61. package/scripts/twofish_diagnostics.py +48 -31
  62. package/scripts/usage_ledger.py +218 -0
  63. package/scripts/vendor/README.md +44 -37
  64. package/scripts/vendor/twofish_pure.py +321 -0
  65. package/scripts/workspace_repair.py +548 -508
  66. package/templates/pt900/donors/README.md +15 -0
@@ -1,1319 +1,1842 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import asdict, dataclass, field
4
- from typing import Any
5
-
6
- from intent_parser import IntentPlan
7
- from sample_catalog import SampleDescriptor
8
-
9
- MATURITY_LEVELS = [
10
- "reference-known",
11
- "inventory-supported",
12
- "edit-supported",
13
- "config-mutation-supported",
14
- "safe-open-generate-supported",
15
- "acceptance-verified",
16
- ]
17
- MATURITY_RANK = {level: index for index, level in enumerate(MATURITY_LEVELS)}
18
-
19
- DEVICE_FAMILY_MAP = {
20
- "Router": "routers",
21
- "Switch": "switches",
22
- "MultiLayerSwitch": "multilayer switches",
23
- "Server": "servers",
24
- "PC": "end devices",
25
- "Laptop": "end devices",
26
- "Tablet": "end devices",
27
- "Smartphone": "end devices",
28
- "Printer": "end devices",
29
- "LightWeightAccessPoint": "access points",
30
- "WirelessRouter": "home/wireless routers",
31
- "WirelessRouterNewGeneration": "home/wireless routers",
32
- "HomeGateway": "home/wireless routers",
33
- "WirelessLanController": "access points",
34
- "Power Distribution Device": "pt-specific edge/utility devices",
35
- "Cloud": "wan/cloud/dsl/cable devices",
36
- "Cable Modem": "wan/cloud/dsl/cable devices",
37
- "Dsl Modem": "wan/cloud/dsl/cable devices",
38
- "Security Appliance": "security devices",
39
- "ASA": "security devices",
40
- "NetworkController": "network controllers",
41
- "CentralOfficeServer": "wan/cloud/dsl/cable devices",
42
- "CellTower": "wan/cloud/dsl/cable devices",
43
- "IoT": "iot devices",
44
- "Board": "iot devices",
45
- "Sensor": "iot devices",
46
- "Actuator": "iot devices",
47
- "MCUComponent": "iot devices",
48
- }
49
-
50
- DEFAULT_FAMILY_LIMITATIONS = {
51
- "access points": ["wireless client association requires donor-backed acceptance"],
52
- "home/wireless routers": ["wireless client association and local NAT flows are acceptance-gated"],
53
- "iot devices": ["iot registration/control is not acceptance-verified yet"],
54
- "wan/cloud/dsl/cable devices": ["advanced wan/cloud patterns depend on donor availability"],
55
- "security devices": ["advanced security devices remain donor-limited"],
56
- }
57
-
58
- REPORT_ONLY_CAPABILITIES = {
59
- "ipv6_slaac",
60
- "dhcpv6_stateful",
61
- "dhcpv6_stateless",
62
- "ipv6_prefix_delegation",
63
- "ipv6_dns_aaaa",
64
- "ipv6_tunneling",
65
- "isatap",
66
- "ospfv3",
67
- "eigrp_ipv6",
68
- "ripng",
69
- "hsrp",
70
- "dhcp_snooping",
71
- "dai",
72
- "dot1x",
73
- "lldp",
74
- "rep",
75
- "snmp",
76
- "netflow",
77
- "span",
78
- "qos",
79
- "port_security",
80
- "asa_acl_nat",
81
- "asa_service_policy",
82
- "clientless_vpn",
83
- "cbac",
84
- "zfw",
85
- "sniffer",
86
- "wlc",
87
- "wpa_enterprise",
88
- "wep",
89
- "guest_wifi",
90
- "beamforming",
91
- "meraki",
92
- "cellular_5g",
93
- "bluetooth",
94
- "network_controller",
95
- "python_programming",
96
- "javascript_programming",
97
- "blockly_programming",
98
- "tcp_udp_app",
99
- "vm_iox",
100
- "voip",
101
- "ip_phone",
102
- "call_manager",
103
- "linksys_voice",
104
- "mqtt",
105
- "real_http",
106
- "real_websocket",
107
- "visual_scripting",
108
- "ptp",
109
- "profinet",
110
- "l2nat",
111
- "cyberobserver",
112
- "industrial_firewall",
113
- "coaxial",
114
- "cable_dsl",
115
- "central_office",
116
- "cell_tower",
117
- "power_distribution",
118
- "hot_swappable",
119
- "ios_license",
120
- }
121
-
122
- IPV6_DONOR_BACKED_CAPABILITIES = {
123
- "ipv6_slaac",
124
- "dhcpv6_stateful",
125
- "ospfv3",
126
- "eigrp_ipv6",
127
- "ripng",
128
- "hsrp",
129
- }
130
- L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES = {
131
- "dhcp_snooping",
132
- "dai",
133
- "lldp",
134
- "rep",
135
- "snmp",
136
- "netflow",
137
- "span",
138
- "port_security",
139
- }
140
- WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES = {
141
- "wep",
142
- "wpa_enterprise",
143
- }
144
- EDIT_PROVEN_REPORT_CAPABILITIES = IPV6_DONOR_BACKED_CAPABILITIES | L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES
145
-
146
- CAPABILITY_PROVIDER_FAMILIES = {
147
- "access_port": {"switches", "multilayer switches"},
148
- "trunk": {"switches", "multilayer switches"},
149
- "native_vlan": {"switches", "multilayer switches"},
150
- "vlan": {"switches", "multilayer switches", "routers"},
151
- "switching": {"switches", "multilayer switches"},
152
- "router_on_a_stick": {"routers", "multilayer switches"},
153
- "management": {"switches", "multilayer switches", "routers"},
154
- "management_vlan": {"switches", "multilayer switches"},
155
- "telnet": {"switches", "multilayer switches", "routers"},
156
- "ssh": {"switches", "multilayer switches", "routers"},
157
- "nat": {"routers", "home/wireless routers", "security devices"},
158
- "pat": {"routers", "home/wireless routers", "security devices"},
159
- "acl": {"routers", "multilayer switches", "switches", "security devices"},
160
- "vpn": {"routers", "security devices", "wan/cloud/dsl/cable devices"},
161
- "ipsec": {"routers", "security devices", "wan/cloud/dsl/cable devices"},
162
- "gre": {"routers", "wan/cloud/dsl/cable devices"},
163
- "ppp": {"routers", "wan/cloud/dsl/cable devices"},
164
- "multilayer_switching": {"multilayer switches"},
165
- "wan": {"wan/cloud/dsl/cable devices", "routers"},
166
- "security_edge": {"security devices", "routers"},
167
- "wireless": {"access points", "home/wireless routers", "end devices"},
168
- "wireless_ap": {"access points", "home/wireless routers"},
169
- "wireless_mutation": {"access points", "home/wireless routers"},
170
- "wireless_client": {"end devices", "access points", "home/wireless routers"},
171
- "wireless_client_association": {"end devices", "access points", "home/wireless routers"},
172
- "end_device_mutation": {"end devices"},
173
- "dhcp": {"routers", "servers", "home/wireless routers"},
174
- "dhcp_pool": {"routers", "servers", "home/wireless routers"},
175
- "router_dhcp": {"routers", "home/wireless routers"},
176
- "server_dhcp": {"servers"},
177
- "dns": {"servers", "routers"},
178
- "server_dns": {"servers"},
179
- "server_http": {"servers"},
180
- "server_ftp": {"servers"},
181
- "server_https": {"servers"},
182
- "server_tftp": {"servers"},
183
- "server_email": {"servers"},
184
- "server_syslog": {"servers"},
185
- "server_aaa": {"servers"},
186
- "ntp": {"servers", "routers", "switches", "multilayer switches"},
187
- "iot": {"iot devices"},
188
- "iot_registration": {"iot devices"},
189
- "iot_control": {"iot devices"},
190
- "tablet": {"end devices"},
191
- "printer": {"end devices"},
192
- "verification": {"routers", "switches", "multilayer switches", "servers", "end devices", "access points"},
193
- "ipv6_slaac": {"routers", "switches", "end devices", "pt-specific edge/utility devices"},
194
- "dhcpv6_stateful": {"routers", "servers", "end devices"},
195
- "dhcpv6_stateless": {"routers", "servers", "end devices"},
196
- "ipv6_prefix_delegation": {"routers", "wan/cloud/dsl/cable devices"},
197
- "ipv6_dns_aaaa": {"servers", "routers"},
198
- "ipv6_tunneling": {"routers"},
199
- "isatap": {"routers", "end devices"},
200
- "ospfv3": {"routers"},
201
- "eigrp_ipv6": {"routers"},
202
- "ripng": {"routers"},
203
- "hsrp": {"routers", "switches", "multilayer switches"},
204
- "dhcp_snooping": {"switches", "multilayer switches"},
205
- "dai": {"switches", "multilayer switches"},
206
- "dot1x": {"switches", "servers", "end devices"},
207
- "lldp": {"routers", "switches", "multilayer switches"},
208
- "rep": {"switches", "multilayer switches"},
209
- "snmp": {"routers", "switches", "multilayer switches", "access points", "servers"},
210
- "netflow": {"routers"},
211
- "span": {"switches", "multilayer switches"},
212
- "qos": {"routers", "switches", "multilayer switches"},
213
- "port_security": {"switches", "multilayer switches"},
214
- "asa_acl_nat": {"security devices"},
215
- "asa_service_policy": {"security devices"},
216
- "clientless_vpn": {"security devices"},
217
- "cbac": {"routers", "security devices"},
218
- "zfw": {"routers", "security devices"},
219
- "sniffer": {"end devices", "pt-specific edge/utility devices"},
220
- "wlc": {"access points"},
221
- "wpa_enterprise": {"access points", "home/wireless routers", "servers"},
222
- "wep": {"access points", "home/wireless routers"},
223
- "guest_wifi": {"home/wireless routers", "access points"},
224
- "beamforming": {"home/wireless routers"},
225
- "meraki": {"security devices", "home/wireless routers", "access points"},
226
- "cellular_5g": {"wan/cloud/dsl/cable devices", "end devices"},
227
- "bluetooth": {"iot devices", "end devices"},
228
- "network_controller": {"pt-specific edge/utility devices", "routers", "switches"},
229
- "python_programming": {"end devices", "pt-specific edge/utility devices"},
230
- "javascript_programming": {"end devices", "pt-specific edge/utility devices"},
231
- "blockly_programming": {"end devices", "pt-specific edge/utility devices"},
232
- "tcp_udp_app": {"end devices", "servers"},
233
- "vm_iox": {"routers", "pt-specific edge/utility devices"},
234
- "voip": {"end devices", "switches", "routers"},
235
- "ip_phone": {"end devices"},
236
- "call_manager": {"routers", "servers", "end devices", "switches"},
237
- "linksys_voice": {"home/wireless routers", "end devices"},
238
- "mqtt": {"iot devices", "servers", "home/wireless routers", "pt-specific edge/utility devices"},
239
- "real_http": {"iot devices", "servers", "pt-specific edge/utility devices"},
240
- "real_websocket": {"iot devices", "servers", "pt-specific edge/utility devices"},
241
- "visual_scripting": {"iot devices"},
242
- "ptp": {"switches", "multilayer switches", "routers"},
243
- "profinet": {"iot devices", "routers", "switches"},
244
- "l2nat": {"switches", "multilayer switches"},
245
- "cyberobserver": {"servers", "switches", "routers"},
246
- "industrial_firewall": {"security devices", "routers"},
247
- "coaxial": {"pt-specific edge/utility devices"},
248
- "cable_dsl": {"wan/cloud/dsl/cable devices"},
249
- "central_office": {"pt-specific edge/utility devices"},
250
- "cell_tower": {"pt-specific edge/utility devices", "wan/cloud/dsl/cable devices"},
251
- "power_distribution": {"pt-specific edge/utility devices"},
252
- "hot_swappable": {"multilayer switches", "pt-specific edge/utility devices"},
253
- "ios_license": {"routers", "switches", "multilayer switches"},
254
- }
255
-
256
-
257
- @dataclass
258
- class CapabilityMatrixEntry:
259
- device_family: str
260
- capability: str
261
- maturity_level: str
262
- accepted_donors: list[str]
263
- known_limitations: list[str]
264
-
265
-
266
- @dataclass
267
- class CoverageGapReport:
268
- requested_capabilities: list[str]
269
- supported_capabilities: list[str]
270
- unsupported_capabilities: list[str]
271
- requested_device_families: list[str]
272
- unsupported_device_families: list[str]
273
- requires_curated_donor: list[str]
274
- requires_manual_acceptance: list[str]
275
- capability_statuses: list[dict[str, object]]
276
- capability_parity: list[dict[str, object]]
277
- scenario_family: str | None = None
278
- scenario_generate_readiness: dict[str, object] = field(default_factory=dict)
279
- recommended_next_actions: list[str] = field(default_factory=list)
280
-
281
-
282
- @dataclass
283
- class BlueprintPlan:
284
- requested_devices: list[dict[str, object]]
285
- requested_links: list[dict[str, object]]
286
- required_capabilities: list[str]
287
- required_donor_capacity: dict[str, int]
288
- required_link_pairs: list[str]
289
-
290
-
291
- @dataclass
292
- class DonorGraphFit:
293
- matched_pairs: list[str]
294
- missing_pairs: list[str]
295
- port_media_conflicts: list[str]
296
- fit_score: int
297
- layout_reuse_score: int
298
-
299
-
300
- def _device_family_for_type(device_type: str) -> str:
301
- return DEVICE_FAMILY_MAP.get(device_type, "pt-specific edge/utility devices")
302
-
303
-
304
- def _sample_maturity(sample: SampleDescriptor) -> str:
305
- if sample.origin == "cisco-local" and sample.prototype_eligible and sample.version.startswith("9."):
306
- structural = "acceptance-verified"
307
- elif sample.origin in {"compat-donor", "external-curated"} and sample.donor_eligible and sample.prototype_eligible:
308
- structural = "safe-open-generate-supported"
309
- elif sample.capability_tags:
310
- structural = "config-mutation-supported"
311
- elif sample.devices:
312
- structural = "inventory-supported"
313
- else:
314
- structural = "reference-known"
315
- declared = sample.apply_safety_level if sample.apply_safety_level in MATURITY_RANK else "reference-known"
316
- if MATURITY_RANK[declared] > MATURITY_RANK[structural]:
317
- return declared
318
- return structural
319
-
320
-
321
- def _merge_limitations(existing: list[str], new_items: list[str]) -> list[str]:
322
- merged = list(existing)
323
- for item in new_items:
324
- if item and item not in merged:
325
- merged.append(item)
326
- return merged
327
-
328
-
329
- def _expanded_sample_capabilities(sample: SampleDescriptor, device_families: list[str]) -> list[str]:
330
- capabilities = set(sample.capability_tags)
331
- service_capability_map = {
332
- "dhcp": {"dhcp", "server_dhcp"},
333
- "dns": {"dns", "server_dns"},
334
- "http": {"server_http"},
335
- "https": {"server_https"},
336
- "ftp": {"server_ftp"},
337
- "tftp": {"server_tftp"},
338
- "email": {"server_email"},
339
- "syslog": {"server_syslog"},
340
- "aaa": {"server_aaa"},
341
- "ntp": {"ntp"},
342
- }
343
- for service in sample.service_support:
344
- capabilities.add(service)
345
- capabilities.update(service_capability_map.get(service, set()))
346
- families = set(device_families)
347
- topology_tags = set(sample.topology_tags)
348
- normalized_counts = sample.normalized_device_counts()
349
- iot_roles = set(sample.iot_roles)
350
-
351
- if "vlan" in capabilities and families & {"switches", "multilayer switches"}:
352
- capabilities.update({"trunk", "access_port"})
353
- if (
354
- "router_on_a_stick" in capabilities
355
- or "router_on_a_stick" in topology_tags
356
- or ("vlan" in capabilities and {"routers", "switches"} <= families)
357
- ):
358
- capabilities.add("router_on_a_stick")
359
- if "management_vlan" in capabilities:
360
- capabilities.add("telnet")
361
- if "telnet" in capabilities and families & {"switches", "multilayer switches"}:
362
- capabilities.add("management_vlan")
363
- if normalized_counts.get("Printer", 0) > 0 or ("end devices" in families and "host_server" in capabilities):
364
- capabilities.add("printer")
365
- if "end devices" in families:
366
- capabilities.add("end_device_mutation")
367
- if sample.wireless_mode_tags and {"access points", "home/wireless routers"} & families and "wireless_ap" in capabilities:
368
- capabilities.add("wireless_mutation")
369
- capabilities.add("wireless_client")
370
- capabilities.add("wireless_client_association")
371
- if iot_roles:
372
- capabilities.add("iot")
373
- if "thing" in iot_roles and ("server" in iot_roles or "gateway" in iot_roles):
374
- capabilities.add("iot_registration")
375
- if "thing" in iot_roles and ("gateway" in iot_roles or "server" in iot_roles):
376
- capabilities.add("iot_control")
377
- if sample.link_count > 0 and families & {"routers", "switches", "multilayer switches", "servers", "end devices", "access points"}:
378
- capabilities.add("verification")
379
- if "multilayer switches" in families:
380
- capabilities.add("multilayer_switching")
381
- if "wan/cloud/dsl/cable devices" in families:
382
- capabilities.add("wan")
383
- if "security devices" in families:
384
- capabilities.add("security_edge")
385
- if "vpn" in capabilities:
386
- capabilities.update({"ipsec", "gre"})
387
- return sorted(capabilities)
388
-
389
-
390
- def build_capability_matrix(samples: list[SampleDescriptor]) -> list[CapabilityMatrixEntry]:
391
- entries: dict[tuple[str, str], CapabilityMatrixEntry] = {}
392
- for sample in samples:
393
- maturity = _sample_maturity(sample)
394
- device_families = sorted({_device_family_for_type(device.get("type", "")) for device in sample.devices if device.get("type")})
395
- capabilities = _expanded_sample_capabilities(sample, device_families)
396
- limitations: list[str] = []
397
- if not sample.donor_eligible:
398
- limitations.append("not donor-eligible")
399
- if not sample.prototype_eligible:
400
- limitations.append("reference-only")
401
- for family in device_families or ["pt-specific edge/utility devices"]:
402
- known_family_limitations = DEFAULT_FAMILY_LIMITATIONS.get(family, [])
403
- for capability in capabilities or ["inventory"]:
404
- key = (family, capability)
405
- donor_label = sample.relative_path
406
- if key not in entries:
407
- entries[key] = CapabilityMatrixEntry(
408
- device_family=family,
409
- capability=capability,
410
- maturity_level=maturity,
411
- accepted_donors=[donor_label],
412
- known_limitations=_merge_limitations([], [*known_family_limitations, *limitations]),
413
- )
414
- continue
415
- current = entries[key]
416
- if MATURITY_RANK[maturity] > MATURITY_RANK[current.maturity_level]:
417
- current.maturity_level = maturity
418
- if donor_label not in current.accepted_donors:
419
- current.accepted_donors.append(donor_label)
420
- current.known_limitations = _merge_limitations(current.known_limitations, [*known_family_limitations, *limitations])
421
- return sorted(entries.values(), key=lambda item: (item.device_family, item.capability))
422
-
423
-
424
- def _requested_device_families(plan: IntentPlan) -> list[str]:
425
- families = {_device_family_for_type(device_type) for device_type, count in plan.device_requirements.items() if count}
426
- return sorted(families)
427
-
428
-
429
- def _scenario_family_for_plan(plan: IntentPlan, requested_families: list[str]) -> str | None:
430
- capabilities = set(plan.capabilities)
431
- requested_services = {str(service) for service in plan.service_requirements.get("services", []) if service}
432
- requested_family_set = set(requested_families)
433
- prompt_lower = str(plan.prompt or "").lower()
434
- campus_capabilities = {"vlan", "router_on_a_stick", "management_vlan", "telnet", "acl", "nat", "pat", "trunk", "access_port"}
435
- service_capabilities = {"server_dns", "server_dhcp", "server_http", "server_https", "server_ftp", "server_tftp", "server_email", "server_syslog", "server_aaa"}
436
- explicit_campus_style = plan.network_style in {"campus", "department_lan"}
437
- explicit_service_style = plan.network_style == "service_heavy"
438
- explicit_home_style = plan.network_style in {"home_iot", "smart_home"}
439
- explicit_home_gateway_prompt = any(
440
- token in prompt_lower
441
- for token in ["home gateway", "homegateway", "smart home", "iot "]
442
- )
443
- campus_signal = (
444
- explicit_campus_style
445
- or bool(plan.department_groups)
446
- or "campus" in prompt_lower
447
- or "kampus" in prompt_lower
448
- or (requested_family_set & {"switches", "multilayer switches"} and capabilities & campus_capabilities)
449
- or len(capabilities & campus_capabilities) >= 3
450
- )
451
- home_iot_signal = (
452
- explicit_home_style
453
- or explicit_home_gateway_prompt
454
- or plan.wireless_mode == "home_router_edge"
455
- or capabilities & {"iot", "iot_registration", "iot_control"}
456
- or {"iot devices", "home/wireless routers"} & requested_family_set
457
- )
458
- wan_signal = (
459
- plan.network_style == "wan_security"
460
- or capabilities & {"vpn", "ipsec", "gre", "ppp", "multilayer_switching", "security_edge"}
461
- or {"wan/cloud/dsl/cable devices", "security devices"} & requested_family_set
462
- )
463
- ipv6_routing_signal = plan.network_style == "ipv6_routing" or capabilities & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ipv6_prefix_delegation", "ipv6_dns_aaaa", "ipv6_tunneling", "isatap", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}
464
- l2_security_monitoring_signal = plan.network_style == "l2_security_monitoring" or capabilities & {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"}
465
- wireless_advanced_signal = plan.network_style == "wireless_advanced" or capabilities & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"}
466
- automation_controller_signal = plan.network_style == "automation_controller" or capabilities & {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"}
467
- voice_collaboration_signal = plan.network_style == "voice_collaboration" or capabilities & {"voip", "ip_phone", "call_manager", "linksys_voice"}
468
- industrial_iot_signal = plan.network_style == "industrial_iot" or capabilities & {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"}
469
- physical_media_device_signal = plan.network_style == "physical_media_device" or capabilities & {"coaxial", "cable_dsl", "central_office", "cell_tower", "power_distribution", "hot_swappable", "ios_license"}
470
- service_signal = (
471
- explicit_service_style
472
- or requested_services & {"dns", "dhcp", "http", "https", "ftp", "tftp", "email", "syslog", "aaa", "ntp"}
473
- or capabilities & service_capabilities
474
- or "servers" in requested_family_set
475
- )
476
- if industrial_iot_signal:
477
- return "industrial_iot"
478
- if automation_controller_signal:
479
- return "automation_controller"
480
- if voice_collaboration_signal:
481
- return "voice_collaboration"
482
- if wireless_advanced_signal:
483
- return "wireless_advanced"
484
- if l2_security_monitoring_signal:
485
- return "l2_security_monitoring"
486
- if ipv6_routing_signal:
487
- return "ipv6_routing"
488
- if physical_media_device_signal:
489
- return "physical_media_device"
490
- if home_iot_signal:
491
- return "home_iot"
492
- if wan_signal:
493
- return "wan_security_edge"
494
- if campus_signal:
495
- return "campus"
496
- if service_signal:
497
- return "service_heavy"
498
- return None
499
-
500
-
501
- SCENARIO_CAPABILITY_SETS = {
502
- "campus": {"router_on_a_stick", "trunk", "access_port", "management_vlan", "telnet", "verification", "acl", "nat"},
503
- "service_heavy": {"server_dns", "server_dhcp", "server_http", "server_https", "server_ftp", "server_tftp", "server_email", "server_syslog", "server_aaa", "ntp"},
504
- "home_iot": {"iot", "iot_registration", "iot_control", "wireless_ap", "wireless_mutation"},
505
- "wan_security_edge": {"vpn", "ipsec", "gre", "ppp", "acl", "nat"},
506
- "ipv6_routing": {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ipv6_prefix_delegation", "ipv6_dns_aaaa", "ipv6_tunneling", "isatap", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"},
507
- "l2_security_monitoring": {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"},
508
- "wireless_advanced": {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"},
509
- "automation_controller": {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"},
510
- "voice_collaboration": {"voip", "ip_phone", "call_manager", "linksys_voice"},
511
- "industrial_iot": {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"},
512
- "physical_media_device": {"coaxial", "cable_dsl", "central_office", "cell_tower", "power_distribution", "hot_swappable", "ios_license"},
513
- }
514
-
515
- CAPABILITY_REQUIRED_ARCHETYPES = {
516
- "router_on_a_stick": ["campus/core"],
517
- "management_vlan": ["campus/core"],
518
- "telnet": ["campus/core"],
519
- "server_dns": ["service-heavy"],
520
- "server_dhcp": ["service-heavy"],
521
- "server_http": ["service-heavy"],
522
- "server_https": ["service-heavy"],
523
- "server_ftp": ["service-heavy"],
524
- "server_tftp": ["service-heavy"],
525
- "server_email": ["service-heavy"],
526
- "server_syslog": ["service-heavy"],
527
- "server_aaa": ["service-heavy"],
528
- "iot_registration": ["IoT/home gateway"],
529
- "iot_control": ["IoT/home gateway"],
530
- "wireless_mutation": ["wireless-heavy"],
531
- "wireless_client_association": ["wireless-heavy", "IoT/home gateway"],
532
- "vpn": ["WAN/security edge"],
533
- "ipsec": ["WAN/security edge"],
534
- "gre": ["WAN/security edge"],
535
- "ppp": ["WAN/security edge"],
536
- "multilayer_switching": ["WAN/security edge", "campus/core"],
537
- "security_edge": ["WAN/security edge"],
538
- "ipv6_slaac": ["IPv6/routing"],
539
- "dhcpv6_stateful": ["IPv6/routing"],
540
- "dhcpv6_stateless": ["IPv6/routing"],
541
- "ipv6_prefix_delegation": ["IPv6/routing"],
542
- "ipv6_dns_aaaa": ["IPv6/routing"],
543
- "ipv6_tunneling": ["IPv6/routing"],
544
- "isatap": ["IPv6/routing"],
545
- "ospfv3": ["IPv6/routing"],
546
- "eigrp_ipv6": ["IPv6/routing"],
547
- "ripng": ["IPv6/routing"],
548
- "hsrp": ["IPv6/routing"],
549
- "dhcp_snooping": ["L2 security/monitoring"],
550
- "dai": ["L2 security/monitoring"],
551
- "dot1x": ["L2 security/monitoring"],
552
- "lldp": ["L2 security/monitoring"],
553
- "rep": ["L2 security/monitoring"],
554
- "snmp": ["L2 security/monitoring"],
555
- "netflow": ["L2 security/monitoring"],
556
- "span": ["L2 security/monitoring"],
557
- "qos": ["L2 security/monitoring"],
558
- "port_security": ["L2 security/monitoring"],
559
- "wlc": ["advanced wireless"],
560
- "wpa_enterprise": ["advanced wireless"],
561
- "wep": ["advanced wireless"],
562
- "guest_wifi": ["advanced wireless"],
563
- "beamforming": ["advanced wireless"],
564
- "meraki": ["advanced wireless"],
565
- "cellular_5g": ["advanced wireless"],
566
- "bluetooth": ["advanced wireless"],
567
- "network_controller": ["automation/controller"],
568
- "python_programming": ["automation/controller"],
569
- "javascript_programming": ["automation/controller"],
570
- "blockly_programming": ["automation/controller"],
571
- "tcp_udp_app": ["automation/controller"],
572
- "vm_iox": ["automation/controller"],
573
- "voip": ["voice/collaboration"],
574
- "ip_phone": ["voice/collaboration"],
575
- "call_manager": ["voice/collaboration"],
576
- "linksys_voice": ["voice/collaboration"],
577
- "mqtt": ["industrial IoT"],
578
- "real_http": ["industrial IoT"],
579
- "real_websocket": ["industrial IoT"],
580
- "visual_scripting": ["industrial IoT"],
581
- "ptp": ["industrial IoT"],
582
- "profinet": ["industrial IoT"],
583
- "l2nat": ["industrial IoT"],
584
- "cyberobserver": ["industrial IoT"],
585
- "industrial_firewall": ["industrial IoT"],
586
- }
587
-
588
- CAPABILITY_REQUIRED_RUNTIME_FEATURES = {
589
- "server_dns": ["workspace_validated", "server_runtime"],
590
- "server_dhcp": ["workspace_validated", "server_runtime"],
591
- "server_http": ["workspace_validated", "server_runtime"],
592
- "server_https": ["workspace_validated", "server_runtime"],
593
- "server_ftp": ["workspace_validated", "server_runtime"],
594
- "server_tftp": ["workspace_validated", "server_runtime"],
595
- "server_email": ["workspace_validated", "server_runtime"],
596
- "server_syslog": ["workspace_validated", "server_runtime"],
597
- "server_aaa": ["workspace_validated", "server_runtime"],
598
- "wireless_mutation": ["workspace_validated", "wireless_runtime"],
599
- "wireless_client_association": ["workspace_validated", "wireless_runtime"],
600
- "iot_registration": ["workspace_validated", "iot_runtime"],
601
- "iot_control": ["workspace_validated", "iot_runtime"],
602
- "vpn": ["workspace_validated", "tunnel_runtime"],
603
- "ipsec": ["workspace_validated", "tunnel_runtime"],
604
- "gre": ["workspace_validated", "tunnel_runtime"],
605
- "ppp": ["workspace_validated", "wan_runtime"],
606
- "security_edge": ["workspace_validated", "security_runtime"],
607
- "multilayer_switching": ["workspace_validated", "multilayer_runtime"],
608
- "ipv6_slaac": ["workspace_validated", "ipv6_runtime"],
609
- "dhcpv6_stateful": ["workspace_validated", "ipv6_runtime"],
610
- "ospfv3": ["workspace_validated", "ipv6_runtime"],
611
- "eigrp_ipv6": ["workspace_validated", "ipv6_runtime"],
612
- "ripng": ["workspace_validated", "ipv6_runtime"],
613
- "hsrp": ["workspace_validated", "ipv6_runtime"],
614
- "wep": ["workspace_validated", "wireless_runtime"],
615
- "wpa_enterprise": ["workspace_validated", "wireless_runtime"],
616
- }
617
-
618
-
619
- def _generate_mismatch_reason(status: dict[str, object]) -> str | None:
620
- if bool(status.get("report_only")):
621
- return "report_only"
622
- if not bool(status.get("inventory_supported")):
623
- return "unsupported"
624
- if bool(status.get("edit_supported")) and not bool(status.get("safe_open_generate_supported")):
625
- return "supported_in_edit_only"
626
- if bool(status.get("safe_open_generate_supported")) and bool(status.get("requires_curated_donor")):
627
- return "supported_but_donor_limited"
628
- if bool(status.get("safe_open_generate_supported")) and bool(status.get("requires_manual_acceptance")) and not bool(status.get("acceptance_verified")):
629
- return "supported_but_acceptance_gated"
630
- if bool(status.get("safe_open_generate_supported")):
631
- return None
632
- return "unsupported"
633
-
634
-
635
- def _selected_sample_for_donor(
636
- samples: list[SampleDescriptor],
637
- selected_donor: str | None,
638
- ) -> SampleDescriptor | None:
639
- if not selected_donor:
640
- return None
641
- needle = str(selected_donor).replace("\\", "/").lower()
642
- for sample in samples:
643
- candidates = {
644
- str(sample.path).replace("\\", "/").lower(),
645
- str(sample.relative_path).replace("\\", "/").lower(),
646
- str(sample.path).replace("\\", "/").split("/")[-1].lower(),
647
- str(sample.relative_path).replace("\\", "/").split("/")[-1].lower(),
648
- }
649
- if needle in candidates:
650
- return sample
651
- return None
652
-
653
-
654
- def _has_explicit_iot_registration(plan: IntentPlan) -> bool:
655
- return any(
656
- op.get("op") == "set_iot_registration"
657
- and str(op.get("device", "")).strip()
658
- and str(op.get("target", "")).strip()
659
- for op in plan.iot_ops
660
- )
661
-
662
-
663
- def _has_explicit_iot_control(plan: IntentPlan) -> bool:
664
- return any(
665
- op.get("op") == "set_iot_rule_state"
666
- and str(op.get("device", "")).strip()
667
- and str(op.get("rule_name", "")).strip()
668
- for op in plan.iot_ops
669
- )
670
-
671
-
672
- def _has_explicit_wireless_association(plan: IntentPlan) -> bool:
673
- return any(
674
- op.get("op") == "associate_wireless_client"
675
- and str(op.get("device", "")).strip()
676
- and str(op.get("ap", "")).strip()
677
- and str(op.get("ssid", "")).strip()
678
- for op in plan.wireless_ops
679
- )
680
-
681
-
682
- def _has_explicit_advanced_wireless_edit(plan: IntentPlan, capability: str) -> bool:
683
- for op in plan.wireless_ops:
684
- if op.get("op") != "set_wireless_ssid":
685
- continue
686
- security = str(op.get("security") or "").strip().lower()
687
- if capability == "wep" and security == "wep" and str(op.get("device") or "").strip() and str(op.get("ssid") or "").strip():
688
- return True
689
- if capability == "wpa_enterprise" and security in {"wpa-enterprise", "wpa2-enterprise", "802.1x"} and str(op.get("device") or "").strip() and str(op.get("ssid") or "").strip():
690
- return True
691
- return False
692
-
693
-
694
- def _has_explicit_wan_security_intent(plan: IntentPlan) -> bool:
695
- capabilities = set(plan.capabilities)
696
- prompt_lower = str(plan.prompt or "").lower()
697
- return (
698
- plan.network_style == "wan_security"
699
- or bool(capabilities & {"vpn", "ipsec", "gre", "ppp", "security_edge", "multilayer_switching"})
700
- or any(token in prompt_lower for token in ["site-to-site", "firewall", "asa", "cloud", "multilayer"])
701
- )
702
-
703
-
704
- def _has_explicit_ipv6_operation(plan: IntentPlan, capability: str) -> bool:
705
- operation_map = {
706
- "ipv6_slaac": {"enable_ipv6_unicast_routing", "set_ipv6_address", "set_ipv6_slaac"},
707
- "dhcpv6_stateful": {"set_dhcpv6_pool"},
708
- "ospfv3": {"set_ospfv3_interface"},
709
- "eigrp_ipv6": {"set_eigrp_ipv6_interface"},
710
- "ripng": {"set_ripng_interface"},
711
- "hsrp": {"set_hsrp_ipv6"},
712
- }
713
- expected_ops = operation_map.get(capability, set())
714
- return any(str(op.get("op")) in expected_ops for op in plan.router_ops)
715
-
716
-
717
- def _selected_donor_capability_override(
718
- plan: IntentPlan,
719
- capability: str,
720
- selected_sample: SampleDescriptor | None,
721
- ) -> bool:
722
- if selected_sample is None:
723
- return False
724
- device_families = sorted({_device_family_for_type(device.get("type", "")) for device in selected_sample.devices if device.get("type")})
725
- sample_capabilities = set(_expanded_sample_capabilities(selected_sample, device_families))
726
- sample_capabilities.update(str(item) for item in selected_sample.validated_edit_capabilities if str(item).strip())
727
- archetypes = {str(item) for item in selected_sample.archetype_tags if str(item).strip()}
728
- runtime_features = {str(item) for item in selected_sample.runtime_features if str(item).strip()}
729
-
730
- if capability == "iot":
731
- return (
732
- bool(archetypes & {"IoT/home gateway"})
733
- and bool(sample_capabilities & {"iot", "iot_registration", "iot_control"})
734
- and bool(runtime_features & {"iot_runtime"})
735
- and (_has_explicit_iot_registration(plan) or _has_explicit_iot_control(plan))
736
- )
737
- if capability == "iot_registration":
738
- return (
739
- "IoT/home gateway" in archetypes
740
- and "iot_registration" in sample_capabilities
741
- and "iot_runtime" in runtime_features
742
- and _has_explicit_iot_registration(plan)
743
- )
744
- if capability == "iot_control":
745
- return (
746
- "IoT/home gateway" in archetypes
747
- and "iot_control" in sample_capabilities
748
- and "iot_runtime" in runtime_features
749
- and _has_explicit_iot_control(plan)
750
- )
751
- if capability == "wireless_client_association":
752
- return (
753
- bool(archetypes & {"IoT/home gateway", "wireless-heavy"})
754
- and "wireless_client_association" in sample_capabilities
755
- and "wireless_runtime" in runtime_features
756
- and _has_explicit_wireless_association(plan)
757
- )
758
- wan_runtime_by_capability = {
759
- "vpn": {"tunnel_runtime", "security_runtime", "wan_runtime"},
760
- "ipsec": {"tunnel_runtime", "security_runtime"},
761
- "gre": {"tunnel_runtime", "wan_runtime"},
762
- "ppp": {"wan_runtime"},
763
- "security_edge": {"security_runtime"},
764
- "multilayer_switching": {"multilayer_runtime"},
765
- }
766
- if capability in wan_runtime_by_capability:
767
- return (
768
- "WAN/security edge" in archetypes
769
- and capability in sample_capabilities
770
- and bool(runtime_features & wan_runtime_by_capability[capability])
771
- and _has_explicit_wan_security_intent(plan)
772
- )
773
- if capability in IPV6_DONOR_BACKED_CAPABILITIES:
774
- return (
775
- "IPv6/routing" in archetypes
776
- and capability in sample_capabilities
777
- and "ipv6_runtime" in runtime_features
778
- and _has_explicit_ipv6_operation(plan, capability)
779
- )
780
- return False
781
-
782
-
783
- def _best_maturity_level(status: dict[str, object]) -> str:
784
- level = str(status.get("best_maturity_level") or "").strip()
785
- if level in MATURITY_RANK:
786
- return level
787
- if bool(status.get("acceptance_verified")):
788
- return "acceptance-verified"
789
- if bool(status.get("safe_open_generate_supported")):
790
- return "safe-open-generate-supported"
791
- if bool(status.get("config_mutation_supported")):
792
- return "config-mutation-supported"
793
- if bool(status.get("edit_supported")):
794
- return "edit-supported"
795
- if bool(status.get("inventory_supported")):
796
- return "inventory-supported"
797
- return "reference-known"
798
-
799
-
800
- def _recommended_next_action_for_capability(capability: str, mismatch_reason: str | None) -> str:
801
- if mismatch_reason == "report_only":
802
- return f"Keep {capability} in atlas/report mode until donor-backed edit or generate evidence exists."
803
- if capability in {"vpn", "ipsec", "gre", "ppp", "security_edge", "multilayer_switching"}:
804
- if mismatch_reason == "supported_but_donor_limited":
805
- return f"Provide a WAN/security edge donor with reusable ASA/cloud/serial or tunnel skeleton for {capability}."
806
- if mismatch_reason == "supported_but_acceptance_gated":
807
- return f"Keep {capability} in report/selection flow until a WAN/security edge donor is acceptance-backed."
808
- return f"Prefer a WAN/security edge donor with explicit {capability} evidence before strict generate."
809
- if capability in IPV6_DONOR_BACKED_CAPABILITIES:
810
- if mismatch_reason == "supported_in_edit_only":
811
- return f"Use explicit edit commands for {capability}; strict prompt generate still needs a selected IPv6/routing donor."
812
- if mismatch_reason == "supported_but_donor_limited":
813
- return f"Provide an IPv6/routing donor with reusable IPv6 interface and routing skeleton for {capability}."
814
- if mismatch_reason == "supported_but_acceptance_gated":
815
- return f"Keep {capability} in report/edit flow until an IPv6/routing donor is acceptance-backed."
816
- return f"Prefer an IPv6/routing donor and explicit router/interface targets before strict generate for {capability}."
817
- if capability in L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES:
818
- if mismatch_reason == "supported_in_edit_only":
819
- return f"Use explicit edit commands for {capability}; strict prompt generate still needs an acceptance-backed L2 security/monitoring donor."
820
- if mismatch_reason == "supported_but_donor_limited":
821
- return f"Provide an L2 security/monitoring donor with reusable switch/router monitoring skeleton for {capability}."
822
- return f"Keep {capability} constrained to edit/inventory proof until an L2 security/monitoring donor is acceptance-backed."
823
- if capability in WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES:
824
- if mismatch_reason == "supported_in_edit_only":
825
- return f"Use explicit wireless edit commands for {capability}; strict prompt generate still needs an acceptance-backed advanced wireless donor."
826
- if mismatch_reason == "supported_but_donor_limited":
827
- return f"Provide an advanced wireless donor with reusable AP/router/WLC skeleton for {capability}."
828
- return f"Keep {capability} constrained to explicit wireless edit proof until an advanced wireless donor is acceptance-backed."
829
- if capability == "iot_registration":
830
- if mismatch_reason == "supported_but_acceptance_gated":
831
- return "Use an IoT/home gateway donor and explicitly name the thing plus gateway/server target before strict generate."
832
- return "Prefer an IoT/home gateway donor and explicitly name the thing plus gateway/server target."
833
- if capability == "iot_control":
834
- if mismatch_reason == "supported_but_acceptance_gated":
835
- return "Use a donor that already contains IoT rules and explicitly name the rule-bearing device before strict generate."
836
- return "Prefer an IoT/home gateway donor with existing rule/control structure before strict generate."
837
- if capability == "wireless_client_association":
838
- if mismatch_reason == "supported_but_acceptance_gated":
839
- return "Use a donor-backed wireless prompt that explicitly names the client, AP/router, and SSID before strict generate."
840
- return "Prefer a wireless-heavy or IoT/home gateway donor and explicitly name the client, AP/router, and SSID."
841
- if mismatch_reason == "supported_in_edit_only":
842
- return f"Use donor-backed edit flow for {capability} until safe-open generate coverage is raised."
843
- if mismatch_reason == "supported_but_donor_limited":
844
- return f"Provide or curate a donor whose skeleton explicitly covers {capability}."
845
- if mismatch_reason == "supported_but_acceptance_gated":
846
- return f"Keep {capability} in explain/edit flow until acceptance evidence is promoted."
847
- return f"Import or curate donor coverage for {capability} before strict generate."
848
-
849
-
850
- def _scenario_generate_readiness(
851
- scenario_family: str | None,
852
- capability_statuses: list[dict[str, object]],
853
- unsupported_capabilities: list[str],
854
- requires_curated_donor: list[str],
855
- requires_manual_acceptance: list[str],
856
- ) -> dict[str, object]:
857
- if not scenario_family:
858
- return {
859
- "family": None,
860
- "status": "not_classified",
861
- "critical_capabilities": [],
862
- "missing_critical_capabilities": [],
863
- "reasons": [],
864
- }
865
- critical_set = SCENARIO_CAPABILITY_SETS.get(scenario_family, set())
866
- status_by_capability = {str(item.get("capability")): item for item in capability_statuses}
867
- relevant_capabilities = [cap for cap in status_by_capability if cap in critical_set]
868
- missing_critical = [cap for cap in unsupported_capabilities if cap in critical_set]
869
- donor_limited_critical = [cap for cap in requires_curated_donor if cap in critical_set]
870
- gated_critical = [cap for cap in requires_manual_acceptance if cap in critical_set and cap not in donor_limited_critical]
871
- reasons: list[str] = []
872
- if missing_critical:
873
- reasons.append(f"missing critical capability coverage: {', '.join(missing_critical)}")
874
- if donor_limited_critical:
875
- reasons.append(f"critical capabilities depend on donor-limited safe-open coverage: {', '.join(donor_limited_critical)}")
876
- if gated_critical:
877
- reasons.append(f"critical capabilities remain acceptance-gated: {', '.join(gated_critical)}")
878
- if missing_critical:
879
- status = "unsupported"
880
- elif donor_limited_critical:
881
- status = "donor_limited"
882
- elif gated_critical:
883
- status = "acceptance_gated"
884
- elif relevant_capabilities:
885
- status = "ready"
886
- else:
887
- status = "partial"
888
- reasons.append("scenario family is recognized, but no critical capability set was requested explicitly.")
889
- return {
890
- "family": scenario_family,
891
- "status": status,
892
- "critical_capabilities": relevant_capabilities,
893
- "missing_critical_capabilities": missing_critical,
894
- "reasons": reasons,
895
- }
896
-
897
-
898
- def _provider_families_for_capability(capability: str, requested_families: list[str]) -> set[str]:
899
- mapped = CAPABILITY_PROVIDER_FAMILIES.get(capability)
900
- if mapped:
901
- return set(mapped)
902
- return set(requested_families)
903
-
904
-
905
- def _normalize_media_name(value: str) -> str:
906
- media = value.strip().lower().replace("_", "-").replace(" ", "-")
907
- aliases = {
908
- "estraightthrough": "straight-through",
909
- "copper-straight-through": "straight-through",
910
- "straight-through": "straight-through",
911
- "ecrossover": "crossover",
912
- "copper-crossover": "crossover",
913
- "crossover": "crossover",
914
- "fiber": "fiber",
915
- "efiber": "fiber",
916
- "serial": "serial",
917
- "eserialdce": "serial-dce",
918
- "serial-dce": "serial-dce",
919
- "eserialdte": "serial-dte",
920
- "serial-dte": "serial-dte",
921
- }
922
- compact = media.replace("-", "")
923
- return aliases.get(media, aliases.get(compact, media))
924
-
925
-
926
- def _pair_key(left: str, right: str) -> str:
927
- return " <-> ".join(sorted((left, right)))
928
-
929
-
930
- def select_capability_matrix_hits(
931
- plan: IntentPlan,
932
- samples: list[SampleDescriptor],
933
- ) -> list[CapabilityMatrixEntry]:
934
- matrix = build_capability_matrix(samples)
935
- requested_capabilities = set(plan.capabilities)
936
- if not requested_capabilities:
937
- return []
938
- requested_families = _requested_device_families(plan)
939
- hits: list[CapabilityMatrixEntry] = []
940
- seen: set[tuple[str, str]] = set()
941
- for entry in matrix:
942
- if entry.capability not in requested_capabilities:
943
- continue
944
- provider_families = _provider_families_for_capability(entry.capability, requested_families)
945
- if provider_families and entry.device_family not in provider_families:
946
- continue
947
- key = (entry.device_family, entry.capability)
948
- if key in seen:
949
- continue
950
- hits.append(entry)
951
- seen.add(key)
952
- return hits
953
-
954
-
955
- def build_blueprint_plan(plan: IntentPlan, blueprint: dict[str, object] | None = None) -> BlueprintPlan:
956
- devices = [dict(device) for device in (blueprint or {}).get("devices", [])]
957
- if not devices:
958
- devices = [
959
- {"type": device_type, "count": count}
960
- for device_type, count in sorted(plan.device_requirements.items())
961
- if count
962
- ]
963
- links = [dict(link) for link in (blueprint or {}).get("links", [])]
964
- if not links:
965
- links = [dict(link) for link in plan.links]
966
- required_pairs: list[str] = []
967
- for link in links:
968
- left = str(link.get("a", {}).get("dev", ""))
969
- right = str(link.get("b", {}).get("dev", ""))
970
- if left and right:
971
- required_pairs.append(" <-> ".join(sorted((left, right))))
972
- return BlueprintPlan(
973
- requested_devices=devices,
974
- requested_links=links,
975
- required_capabilities=sorted(dict.fromkeys(plan.capabilities)),
976
- required_donor_capacity={key: value for key, value in sorted(plan.device_requirements.items()) if value},
977
- required_link_pairs=sorted(dict.fromkeys(required_pairs)),
978
- )
979
-
980
-
981
- def build_coverage_gap_report(
982
- plan: IntentPlan,
983
- samples: list[SampleDescriptor],
984
- *,
985
- selected_donor: str | None = None,
986
- ) -> CoverageGapReport:
987
- iot_acceptance_gated = {"iot", "iot_registration", "iot_control"}
988
- selected_sample = _selected_sample_for_donor(samples, selected_donor)
989
- matrix = build_capability_matrix(samples)
990
- by_capability: dict[str, list[CapabilityMatrixEntry]] = {}
991
- by_family: dict[str, list[CapabilityMatrixEntry]] = {}
992
- for entry in matrix:
993
- by_capability.setdefault(entry.capability, []).append(entry)
994
- by_family.setdefault(entry.device_family, []).append(entry)
995
-
996
- requested_families = _requested_device_families(plan)
997
- requested_capabilities = sorted(dict.fromkeys(plan.capabilities))
998
- supported_capabilities: list[str] = []
999
- unsupported_capabilities: list[str] = []
1000
- requires_curated_donor: list[str] = []
1001
- requires_manual_acceptance: list[str] = []
1002
- capability_statuses: list[dict[str, object]] = []
1003
- for capability in requested_capabilities:
1004
- provider_families = _provider_families_for_capability(capability, requested_families)
1005
- selected_donor_backed = _selected_donor_capability_override(plan, capability, selected_sample)
1006
- wireless_edit_proven_only = (
1007
- capability in WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES
1008
- and _has_explicit_advanced_wireless_edit(plan, capability)
1009
- and not selected_donor_backed
1010
- )
1011
- edit_proven_only = (capability in EDIT_PROVEN_REPORT_CAPABILITIES or wireless_edit_proven_only) and not selected_donor_backed
1012
- report_only = capability in REPORT_ONLY_CAPABILITIES and not selected_donor_backed and not edit_proven_only
1013
- matching_entries = [
1014
- entry
1015
- for entry in by_capability.get(capability, [])
1016
- if not provider_families or entry.device_family in provider_families
1017
- ]
1018
- if matching_entries:
1019
- supported_capabilities.append(capability)
1020
- best_entry = max(matching_entries, key=lambda entry: MATURITY_RANK[entry.maturity_level])
1021
- acceptance_verified = (
1022
- (any(entry.maturity_level == "acceptance-verified" for entry in matching_entries) and capability not in iot_acceptance_gated)
1023
- or selected_donor_backed
1024
- )
1025
- if report_only or edit_proven_only:
1026
- acceptance_verified = False
1027
- requires_curated = best_entry.maturity_level == "safe-open-generate-supported" and not selected_donor_backed and not edit_proven_only
1028
- if requires_curated:
1029
- requires_curated_donor.append(capability)
1030
- if not acceptance_verified:
1031
- requires_manual_acceptance.append(capability)
1032
- capability_statuses.append(
1033
- {
1034
- "capability": capability,
1035
- "provider_families": sorted(provider_families),
1036
- "matching_device_families": sorted({entry.device_family for entry in matching_entries}),
1037
- "best_maturity_level": "inventory-supported" if report_only else "config-mutation-supported" if edit_proven_only else best_entry.maturity_level,
1038
- "inventory_supported": any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["inventory-supported"] for entry in matching_entries),
1039
- "edit_supported": True if edit_proven_only else False if report_only else any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["config-mutation-supported"] for entry in matching_entries),
1040
- "config_mutation_supported": True if edit_proven_only else False if report_only else any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["config-mutation-supported"] for entry in matching_entries),
1041
- "safe_open_generate_supported": False if report_only or edit_proven_only else any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["safe-open-generate-supported"] for entry in matching_entries),
1042
- "acceptance_verified": acceptance_verified,
1043
- "requires_curated_donor": requires_curated,
1044
- "requires_manual_acceptance": not acceptance_verified,
1045
- "report_only": report_only,
1046
- "recommended_donors": list(dict.fromkeys(donor for entry in matching_entries for donor in entry.accepted_donors))[:10],
1047
- "known_limitations": list(dict.fromkeys(item for entry in matching_entries for item in entry.known_limitations)),
1048
- }
1049
- )
1050
- elif edit_proven_only:
1051
- supported_capabilities.append(capability)
1052
- capability_statuses.append(
1053
- {
1054
- "capability": capability,
1055
- "provider_families": sorted(provider_families),
1056
- "matching_device_families": sorted(provider_families),
1057
- "best_maturity_level": "config-mutation-supported",
1058
- "inventory_supported": True,
1059
- "edit_supported": True,
1060
- "config_mutation_supported": True,
1061
- "safe_open_generate_supported": False,
1062
- "acceptance_verified": False,
1063
- "requires_curated_donor": False,
1064
- "requires_manual_acceptance": True,
1065
- "report_only": False,
1066
- "recommended_donors": [],
1067
- "known_limitations": ["editor-proven without sample-backed donor selection"],
1068
- }
1069
- )
1070
- elif report_only:
1071
- supported_capabilities.append(capability)
1072
- capability_statuses.append(
1073
- {
1074
- "capability": capability,
1075
- "provider_families": sorted(provider_families),
1076
- "matching_device_families": sorted(provider_families),
1077
- "best_maturity_level": "inventory-supported",
1078
- "inventory_supported": True,
1079
- "edit_supported": False,
1080
- "config_mutation_supported": False,
1081
- "safe_open_generate_supported": False,
1082
- "acceptance_verified": False,
1083
- "requires_curated_donor": False,
1084
- "requires_manual_acceptance": True,
1085
- "report_only": True,
1086
- "recommended_donors": [],
1087
- "known_limitations": ["report-supported without decode-backed donor selection"],
1088
- }
1089
- )
1090
- elif selected_donor_backed and selected_sample is not None:
1091
- supported_capabilities.append(capability)
1092
- capability_statuses.append(
1093
- {
1094
- "capability": capability,
1095
- "provider_families": sorted(provider_families),
1096
- "matching_device_families": sorted({family for family in (selected_sample.device_families or []) if not provider_families or family in provider_families}),
1097
- "best_maturity_level": selected_sample.apply_safety_level or "safe-open-generate-supported",
1098
- "inventory_supported": True,
1099
- "edit_supported": True,
1100
- "config_mutation_supported": True,
1101
- "safe_open_generate_supported": True,
1102
- "acceptance_verified": True,
1103
- "requires_curated_donor": False,
1104
- "requires_manual_acceptance": False,
1105
- "recommended_donors": [selected_sample.relative_path],
1106
- "known_limitations": [],
1107
- }
1108
- )
1109
- else:
1110
- unsupported_capabilities.append(capability)
1111
- capability_statuses.append(
1112
- {
1113
- "capability": capability,
1114
- "provider_families": sorted(provider_families),
1115
- "matching_device_families": [],
1116
- "best_maturity_level": "reference-known",
1117
- "inventory_supported": False,
1118
- "edit_supported": False,
1119
- "config_mutation_supported": False,
1120
- "safe_open_generate_supported": False,
1121
- "acceptance_verified": False,
1122
- "requires_curated_donor": False,
1123
- "requires_manual_acceptance": True,
1124
- "recommended_donors": [],
1125
- "known_limitations": ["no matching donor coverage found"],
1126
- }
1127
- )
1128
-
1129
- unsupported_families = [family for family in requested_families if family not in by_family]
1130
-
1131
- requires_curated_donor = sorted(dict.fromkeys(requires_curated_donor))
1132
- requires_manual_acceptance = sorted(dict.fromkeys(requires_manual_acceptance))
1133
- capability_parity = [
1134
- {
1135
- "capability": str(status.get("capability")),
1136
- "inventory_supported": bool(status.get("inventory_supported")),
1137
- "edit_supported": bool(status.get("edit_supported")),
1138
- "generate_supported": bool(status.get("safe_open_generate_supported")),
1139
- "acceptance_verified": bool(status.get("acceptance_verified")),
1140
- "best_maturity_level": _best_maturity_level(status),
1141
- "generate_mismatch_reason": _generate_mismatch_reason(status),
1142
- "required_donor_families": sorted(_provider_families_for_capability(str(status.get("capability")), requested_families)),
1143
- "required_archetypes": CAPABILITY_REQUIRED_ARCHETYPES.get(str(status.get("capability")), []),
1144
- "required_runtime_features": CAPABILITY_REQUIRED_RUNTIME_FEATURES.get(str(status.get("capability")), []),
1145
- "recommended_next_action": _recommended_next_action_for_capability(
1146
- str(status.get("capability")),
1147
- _generate_mismatch_reason(status),
1148
- ),
1149
- }
1150
- for status in capability_statuses
1151
- ]
1152
- scenario_family = _scenario_family_for_plan(plan, requested_families)
1153
- if selected_donor is None and plan.blocked_mutations:
1154
- requires_manual_acceptance = sorted(dict.fromkeys([*requires_manual_acceptance, *plan.blocked_mutations]))
1155
- scenario_generate_readiness = _scenario_generate_readiness(
1156
- scenario_family,
1157
- capability_statuses,
1158
- unsupported_capabilities,
1159
- requires_curated_donor,
1160
- requires_manual_acceptance,
1161
- )
1162
- recommended_next_actions: list[str] = []
1163
- if unsupported_capabilities or unsupported_families:
1164
- recommended_next_actions.append(
1165
- "Inspect the blueprint and import or provide a donor that contains the missing capabilities or device families."
1166
- )
1167
- if requires_manual_acceptance:
1168
- recommended_next_actions.append(
1169
- "Run explain-plan or validate-open-debug before enabling the acceptance-gated capability set."
1170
- )
1171
- if not requested_capabilities and not requested_families:
1172
- recommended_next_actions.append("Add explicit device, service, or topology requirements so donor ranking can be constrained.")
1173
-
1174
- return CoverageGapReport(
1175
- requested_capabilities=requested_capabilities,
1176
- supported_capabilities=supported_capabilities,
1177
- unsupported_capabilities=unsupported_capabilities,
1178
- requested_device_families=requested_families,
1179
- unsupported_device_families=unsupported_families,
1180
- requires_curated_donor=requires_curated_donor,
1181
- requires_manual_acceptance=requires_manual_acceptance,
1182
- capability_statuses=capability_statuses,
1183
- capability_parity=capability_parity,
1184
- scenario_family=scenario_family,
1185
- scenario_generate_readiness=scenario_generate_readiness,
1186
- recommended_next_actions=recommended_next_actions,
1187
- )
1188
-
1189
-
1190
- def build_inventory_capability_report(payload: dict[str, Any]) -> dict[str, object]:
1191
- device_counts = dict(payload.get("topology_summary", {}).get("device_counts", {}))
1192
- families = sorted({_device_family_for_type(device_type) for device_type in device_counts})
1193
- capabilities: set[str] = set()
1194
- if "multilayer switches" in families:
1195
- capabilities.add("multilayer_switching")
1196
- if "wan/cloud/dsl/cable devices" in families:
1197
- capabilities.add("wan")
1198
- if "security devices" in families:
1199
- capabilities.add("security_edge")
1200
- if payload.get("vlans"):
1201
- capabilities.update(["vlan", "trunk", "access_port"])
1202
- if payload.get("dhcp_pools"):
1203
- capabilities.update(["dhcp_pool", "router_dhcp", "server_dhcp"])
1204
- if payload.get("services"):
1205
- capabilities.update(
1206
- {
1207
- "dns" if "dns_server" in service else service
1208
- for service_names in payload["services"].values()
1209
- for service in service_names
1210
- }
1211
- )
1212
- if payload.get("wireless"):
1213
- capabilities.update(["wireless_ap", "wireless_mutation", "wireless_client"])
1214
- if any(family == "end devices" for family in families):
1215
- capabilities.add("wireless_client_association")
1216
- if payload.get("iot"):
1217
- capabilities.add("iot")
1218
- iot_roles = {str(entry.get("role", "")).strip() for entry in payload["iot"].values() if str(entry.get("role", "")).strip()}
1219
- if "thing" in iot_roles and ("gateway" in iot_roles or "server" in iot_roles):
1220
- capabilities.add("iot_registration")
1221
- if "thing" in iot_roles and ("gateway" in iot_roles or "server" in iot_roles):
1222
- capabilities.add("iot_control")
1223
- if any(family == "end devices" for family in families):
1224
- capabilities.add("end_device_mutation")
1225
- if payload.get("acl_names"):
1226
- capabilities.add("acl")
1227
- if payload.get("topology_summary", {}).get("network_style") == "wan_security":
1228
- capabilities.update({"vpn", "ipsec", "gre", "ppp"})
1229
- for routing_details in payload.get("routing", {}).values():
1230
- capabilities.update(str(capability) for capability in routing_details.get("capabilities", []) if str(capability).strip())
1231
- for l2_details in payload.get("l2_security_monitoring", {}).values():
1232
- capabilities.update(str(capability) for capability in l2_details.get("capabilities", []) if str(capability).strip())
1233
- return {
1234
- "device_families": families,
1235
- "capabilities": sorted(capabilities),
1236
- "device_counts": device_counts,
1237
- }
1238
-
1239
-
1240
- def build_donor_graph_fit(sample: SampleDescriptor, blueprint: dict[str, object] | None = None) -> DonorGraphFit:
1241
- expected_pairs: list[str] = []
1242
- expected_media: dict[str, str] = {}
1243
- expected_ports: dict[str, set[str]] = {}
1244
- blueprint_archetype = ""
1245
- blueprint_device_count = 0
1246
- if blueprint:
1247
- blueprint_archetype = str(blueprint.get("topology_archetype", "")).strip()
1248
- blueprint_device_count = len(list(blueprint.get("devices", [])))
1249
- for link in blueprint.get("links", []):
1250
- left = str(link.get("a", {}).get("dev", ""))
1251
- right = str(link.get("b", {}).get("dev", ""))
1252
- if left and right:
1253
- pair = _pair_key(left, right)
1254
- expected_pairs.append(pair)
1255
- media = _normalize_media_name(str(link.get("media", "")))
1256
- if media:
1257
- expected_media[pair] = media
1258
- ports = {
1259
- str(link.get("a", {}).get("port", "")).strip(),
1260
- str(link.get("b", {}).get("port", "")).strip(),
1261
- }
1262
- expected_ports[pair] = {port for port in ports if port}
1263
- sample_pairs: set[str] = set()
1264
- sample_media: dict[str, str] = {}
1265
- sample_ports: dict[str, set[str]] = {}
1266
- for link in sample.links:
1267
- left = str(link.get("from", "")).strip()
1268
- right = str(link.get("to", "")).strip()
1269
- if left and right:
1270
- pair = _pair_key(left, right)
1271
- sample_pairs.add(pair)
1272
- media = _normalize_media_name(str(link.get("media", "") or link.get("cable_type", "")))
1273
- if media and pair not in sample_media:
1274
- sample_media[pair] = media
1275
- ports = {str(port).strip() for port in link.get("ports", []) if str(port).strip()}
1276
- if ports:
1277
- sample_ports.setdefault(pair, set()).update(ports)
1278
- matched_pairs = [pair for pair in expected_pairs if pair in sample_pairs]
1279
- missing_pairs = [pair for pair in expected_pairs if pair not in sample_pairs]
1280
- port_media_conflicts: list[str] = []
1281
- for pair in matched_pairs:
1282
- expected_pair_media = expected_media.get(pair)
1283
- sample_pair_media = sample_media.get(pair)
1284
- if expected_pair_media and sample_pair_media and expected_pair_media != sample_pair_media:
1285
- port_media_conflicts.append(f"{pair} media mismatch ({expected_pair_media} != {sample_pair_media})")
1286
- expected_pair_ports = expected_ports.get(pair, set())
1287
- sample_pair_ports = sample_ports.get(pair, set())
1288
- if expected_pair_ports and sample_pair_ports and not expected_pair_ports.issubset(sample_pair_ports):
1289
- port_media_conflicts.append(
1290
- f"{pair} port mismatch (expected {sorted(expected_pair_ports)}, available {sorted(sample_pair_ports)})"
1291
- )
1292
- fit_score = max(0, len(matched_pairs) * 10 - len(missing_pairs) * 5 - len(port_media_conflicts) * 3)
1293
- topology_bonus = 0
1294
- if blueprint_archetype:
1295
- sample_topology = set(sample.topology_tags)
1296
- if blueprint_archetype in sample_topology:
1297
- topology_bonus += 18
1298
- elif blueprint_archetype == "chain" and sample_topology & {"department_lan", "core_access"}:
1299
- topology_bonus += 12
1300
- elif blueprint_archetype == "core_access" and sample_topology & {"department_lan", "router_on_a_stick"}:
1301
- topology_bonus += 10
1302
- elif blueprint_archetype == "wireless_branch" and sample_topology & {"wireless_edge", "small_office"}:
1303
- topology_bonus += 10
1304
- capacity_penalty = max(0, blueprint_device_count - sample.device_count) * 2
1305
- layout_reuse_score = max(
1306
- 0,
1307
- len(matched_pairs) * 12 + topology_bonus - len(missing_pairs) * 6 - len(port_media_conflicts) * 4 - capacity_penalty,
1308
- )
1309
- return DonorGraphFit(
1310
- matched_pairs=matched_pairs,
1311
- missing_pairs=missing_pairs,
1312
- port_media_conflicts=port_media_conflicts,
1313
- fit_score=fit_score,
1314
- layout_reuse_score=layout_reuse_score,
1315
- )
1316
-
1317
-
1318
- def asdict_list(items: list[Any]) -> list[dict[str, object]]:
1319
- return [asdict(item) for item in items]
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import asdict, dataclass, field
5
+ from typing import Any
6
+
7
+ from intent_parser import IntentPlan
8
+ from sample_catalog import SampleDescriptor
9
+
10
+ MATURITY_LEVELS = [
11
+ "reference-known",
12
+ "inventory-supported",
13
+ "edit-supported",
14
+ "config-mutation-supported",
15
+ "safe-open-generate-supported",
16
+ "acceptance-verified",
17
+ ]
18
+ MATURITY_RANK = {level: index for index, level in enumerate(MATURITY_LEVELS)}
19
+
20
+ DEVICE_FAMILY_MAP = {
21
+ "Router": "routers",
22
+ "Switch": "switches",
23
+ "MultiLayerSwitch": "multilayer switches",
24
+ "Server": "servers",
25
+ "PC": "end devices",
26
+ "Laptop": "end devices",
27
+ "Tablet": "end devices",
28
+ "Smartphone": "end devices",
29
+ "Printer": "end devices",
30
+ "IpPhone": "end devices",
31
+ "HomeVoip": "end devices",
32
+ "AnalogPhone": "end devices",
33
+ "LightWeightAccessPoint": "access points",
34
+ "WirelessRouter": "home/wireless routers",
35
+ "WirelessRouterNewGeneration": "home/wireless routers",
36
+ "HomeGateway": "home/wireless routers",
37
+ "WirelessLanController": "access points",
38
+ "Power Distribution Device": "pt-specific edge/utility devices",
39
+ "Cloud": "wan/cloud/dsl/cable devices",
40
+ "Cable Modem": "wan/cloud/dsl/cable devices",
41
+ "Dsl Modem": "wan/cloud/dsl/cable devices",
42
+ "Security Appliance": "security devices",
43
+ "ASA": "security devices",
44
+ "NetworkController": "network controllers",
45
+ "CentralOfficeServer": "wan/cloud/dsl/cable devices",
46
+ "CellTower": "wan/cloud/dsl/cable devices",
47
+ "IoT": "iot devices",
48
+ "Board": "iot devices",
49
+ "Sensor": "iot devices",
50
+ "Actuator": "iot devices",
51
+ "MCUComponent": "iot devices",
52
+ }
53
+
54
+ DEFAULT_FAMILY_LIMITATIONS = {
55
+ "access points": ["wireless client association requires donor-backed acceptance"],
56
+ "home/wireless routers": ["wireless client association and local NAT flows are acceptance-gated"],
57
+ "iot devices": ["iot registration/control is not acceptance-verified yet"],
58
+ "wan/cloud/dsl/cable devices": ["advanced wan/cloud patterns depend on donor availability"],
59
+ "security devices": ["advanced security devices remain donor-limited"],
60
+ }
61
+
62
+ REPORT_ONLY_CAPABILITIES = {
63
+ "ipv6_slaac",
64
+ "dhcpv6_stateful",
65
+ "dhcpv6_stateless",
66
+ "ipv6_prefix_delegation",
67
+ "ipv6_dns_aaaa",
68
+ "ipv6_tunneling",
69
+ "isatap",
70
+ "ospfv3",
71
+ "eigrp_ipv6",
72
+ "ripng",
73
+ "hsrp",
74
+ "dhcp_snooping",
75
+ "dai",
76
+ "dot1x",
77
+ "lldp",
78
+ "rep",
79
+ "snmp",
80
+ "netflow",
81
+ "span",
82
+ "qos",
83
+ "port_security",
84
+ "bgp",
85
+ "stp",
86
+ "rstp",
87
+ "etherchannel",
88
+ "lacp",
89
+ "pagp",
90
+ "vtp",
91
+ "dtp",
92
+ "ospfv2",
93
+ "eigrp_ipv4",
94
+ "ripv2",
95
+ "static_route",
96
+ "default_route",
97
+ "dhcp_relay",
98
+ "nat_static",
99
+ "nat_dynamic",
100
+ "pat",
101
+ "ssh_ios",
102
+ "ntp_ios",
103
+ "syslog_ios",
104
+ "asa_acl_nat",
105
+ "asa_service_policy",
106
+ "clientless_vpn",
107
+ "cbac",
108
+ "zfw",
109
+ "sniffer",
110
+ "wlc",
111
+ "wpa_enterprise",
112
+ "wep",
113
+ "guest_wifi",
114
+ "beamforming",
115
+ "meraki",
116
+ "cellular_5g",
117
+ "bluetooth",
118
+ "network_controller",
119
+ "python_programming",
120
+ "javascript_programming",
121
+ "blockly_programming",
122
+ "tcp_udp_app",
123
+ "vm_iox",
124
+ "voip",
125
+ "ip_phone",
126
+ "call_manager",
127
+ "linksys_voice",
128
+ "mqtt",
129
+ "real_http",
130
+ "real_websocket",
131
+ "visual_scripting",
132
+ "ptp",
133
+ "profinet",
134
+ "l2nat",
135
+ "cyberobserver",
136
+ "industrial_firewall",
137
+ "coaxial",
138
+ "cable_dsl",
139
+ "central_office",
140
+ "cell_tower",
141
+ "power_distribution",
142
+ "hot_swappable",
143
+ "ios_license",
144
+ }
145
+
146
+ IPV6_DONOR_BACKED_CAPABILITIES = {
147
+ "ipv6_slaac",
148
+ "dhcpv6_stateful",
149
+ "ospfv3",
150
+ "eigrp_ipv6",
151
+ "ripng",
152
+ "hsrp",
153
+ }
154
+ IPV6_DONOR_BACKED_READY_EDIT_CAPABILITIES = {
155
+ "ospfv3",
156
+ "eigrp_ipv6",
157
+ "ripng",
158
+ "hsrp",
159
+ }
160
+ L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES = {
161
+ "dhcp_snooping",
162
+ "dai",
163
+ "dot1x",
164
+ "lldp",
165
+ "rep",
166
+ "snmp",
167
+ "netflow",
168
+ "span",
169
+ "qos",
170
+ "port_security",
171
+ }
172
+ WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES = {
173
+ "wep",
174
+ "wpa_enterprise",
175
+ }
176
+ PROGRAMMING_EDIT_PROVEN_CAPABILITIES = {
177
+ "real_http",
178
+ "real_websocket",
179
+ "python_programming",
180
+ "javascript_programming",
181
+ "tcp_udp_app",
182
+ }
183
+ AUTOMATION_CONTROLLER_EDIT_PROVEN_CAPABILITIES = {
184
+ "python_programming",
185
+ "javascript_programming",
186
+ "tcp_udp_app",
187
+ }
188
+ PROGRAMMING_DONOR_BACKED_READY_CAPABILITIES = {
189
+ "real_http",
190
+ "real_websocket",
191
+ "python_programming",
192
+ "javascript_programming",
193
+ "tcp_udp_app",
194
+ }
195
+ L2_SECURITY_MONITORING_DONOR_BACKED_READY_CAPABILITIES = {
196
+ "dot1x",
197
+ }
198
+ WAN_SECURITY_DONOR_BACKED_READY_EDIT_CAPABILITIES = {
199
+ "zfw",
200
+ }
201
+ VOICE_COLLABORATION_DONOR_BACKED_READY_CAPABILITIES = {
202
+ "voip",
203
+ "ip_phone",
204
+ "call_manager",
205
+ }
206
+ VOICE_COLLABORATION_EDIT_PROVEN_CAPABILITIES = {
207
+ "voip",
208
+ "ip_phone",
209
+ "call_manager",
210
+ }
211
+ WAN_SECURITY_EDIT_PROVEN_CAPABILITIES = {
212
+ "gre",
213
+ "ppp",
214
+ "ipsec",
215
+ "vpn",
216
+ "cbac",
217
+ "zfw",
218
+ }
219
+ L2_RESILIENCY_ROUTING_EDIT_PROVEN_CAPABILITIES = {
220
+ "bgp",
221
+ "stp",
222
+ "rstp",
223
+ "etherchannel",
224
+ "lacp",
225
+ "pagp",
226
+ "vtp",
227
+ "dtp",
228
+ }
229
+ IPV4_ROUTING_MANAGEMENT_EDIT_PROVEN_CAPABILITIES = {
230
+ "ospfv2",
231
+ "eigrp_ipv4",
232
+ "ripv2",
233
+ "static_route",
234
+ "default_route",
235
+ "dhcp_relay",
236
+ "nat_static",
237
+ "nat_dynamic",
238
+ "pat",
239
+ "ssh_ios",
240
+ "ntp_ios",
241
+ "syslog_ios",
242
+ }
243
+
244
+ WAN_SECURITY_DONOR_LIMITED_CAPABILITIES = {
245
+ "gre",
246
+ "ppp",
247
+ "ipsec",
248
+ "vpn",
249
+ "security_edge",
250
+ "multilayer_switching",
251
+ }
252
+ ALWAYS_EDIT_PROVEN_REPORT_CAPABILITIES = IPV6_DONOR_BACKED_CAPABILITIES | (
253
+ L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES - {"dot1x", "qos"}
254
+ )
255
+ EDIT_PROVEN_REPORT_CAPABILITIES = ALWAYS_EDIT_PROVEN_REPORT_CAPABILITIES
256
+
257
+ CAPABILITY_PROVIDER_FAMILIES = {
258
+ "access_port": {"switches", "multilayer switches"},
259
+ "trunk": {"switches", "multilayer switches"},
260
+ "native_vlan": {"switches", "multilayer switches"},
261
+ "vlan": {"switches", "multilayer switches", "routers"},
262
+ "switching": {"switches", "multilayer switches"},
263
+ "router_on_a_stick": {"routers", "multilayer switches"},
264
+ "management": {"switches", "multilayer switches", "routers"},
265
+ "management_vlan": {"switches", "multilayer switches"},
266
+ "telnet": {"switches", "multilayer switches", "routers"},
267
+ "ssh": {"switches", "multilayer switches", "routers"},
268
+ "nat": {"routers", "home/wireless routers", "security devices"},
269
+ "pat": {"routers", "home/wireless routers", "security devices"},
270
+ "acl": {"routers", "multilayer switches", "switches", "security devices"},
271
+ "vpn": {"routers", "security devices", "wan/cloud/dsl/cable devices"},
272
+ "ipsec": {"routers", "security devices", "wan/cloud/dsl/cable devices"},
273
+ "gre": {"routers", "wan/cloud/dsl/cable devices"},
274
+ "ppp": {"routers", "wan/cloud/dsl/cable devices"},
275
+ "bgp": {"routers", "multilayer switches"},
276
+ "stp": {"switches", "multilayer switches"},
277
+ "rstp": {"switches", "multilayer switches"},
278
+ "etherchannel": {"switches", "multilayer switches"},
279
+ "lacp": {"switches", "multilayer switches"},
280
+ "pagp": {"switches", "multilayer switches"},
281
+ "vtp": {"switches", "multilayer switches"},
282
+ "dtp": {"switches", "multilayer switches"},
283
+ "ospfv2": {"routers", "multilayer switches"},
284
+ "eigrp_ipv4": {"routers", "multilayer switches"},
285
+ "ripv2": {"routers", "multilayer switches"},
286
+ "static_route": {"routers", "multilayer switches", "security devices"},
287
+ "default_route": {"routers", "multilayer switches", "security devices"},
288
+ "dhcp_relay": {"routers", "multilayer switches"},
289
+ "nat_static": {"routers", "security devices"},
290
+ "nat_dynamic": {"routers", "security devices"},
291
+ "ssh_ios": {"routers", "switches", "multilayer switches"},
292
+ "ntp_ios": {"routers", "switches", "multilayer switches"},
293
+ "syslog_ios": {"routers", "switches", "multilayer switches"},
294
+ "multilayer_switching": {"multilayer switches"},
295
+ "wan": {"wan/cloud/dsl/cable devices", "routers"},
296
+ "security_edge": {"security devices", "routers"},
297
+ "wireless": {"access points", "home/wireless routers", "end devices"},
298
+ "wireless_ap": {"access points", "home/wireless routers"},
299
+ "wireless_mutation": {"access points", "home/wireless routers"},
300
+ "wireless_client": {"end devices", "access points", "home/wireless routers"},
301
+ "wireless_client_association": {"end devices", "access points", "home/wireless routers"},
302
+ "end_device_mutation": {"end devices"},
303
+ "dhcp": {"routers", "servers", "home/wireless routers"},
304
+ "dhcp_pool": {"routers", "servers", "home/wireless routers"},
305
+ "router_dhcp": {"routers", "home/wireless routers"},
306
+ "server_dhcp": {"servers"},
307
+ "dns": {"servers", "routers"},
308
+ "server_dns": {"servers"},
309
+ "server_http": {"servers"},
310
+ "server_ftp": {"servers"},
311
+ "server_https": {"servers"},
312
+ "server_tftp": {"servers"},
313
+ "server_email": {"servers"},
314
+ "server_syslog": {"servers"},
315
+ "server_aaa": {"servers"},
316
+ "ntp": {"servers", "routers", "switches", "multilayer switches"},
317
+ "iot": {"iot devices"},
318
+ "iot_registration": {"iot devices"},
319
+ "iot_control": {"iot devices"},
320
+ "tablet": {"end devices"},
321
+ "printer": {"end devices"},
322
+ "verification": {"routers", "switches", "multilayer switches", "servers", "end devices", "access points"},
323
+ "ipv6_slaac": {"routers", "switches", "end devices", "pt-specific edge/utility devices"},
324
+ "dhcpv6_stateful": {"routers", "servers", "end devices"},
325
+ "dhcpv6_stateless": {"routers", "servers", "end devices"},
326
+ "ipv6_prefix_delegation": {"routers", "wan/cloud/dsl/cable devices"},
327
+ "ipv6_dns_aaaa": {"servers", "routers"},
328
+ "ipv6_tunneling": {"routers"},
329
+ "isatap": {"routers", "end devices"},
330
+ "ospfv3": {"routers"},
331
+ "eigrp_ipv6": {"routers"},
332
+ "ripng": {"routers"},
333
+ "hsrp": {"routers", "switches", "multilayer switches"},
334
+ "dhcp_snooping": {"switches", "multilayer switches"},
335
+ "dai": {"switches", "multilayer switches"},
336
+ "dot1x": {"switches", "servers", "end devices"},
337
+ "lldp": {"routers", "switches", "multilayer switches"},
338
+ "rep": {"switches", "multilayer switches"},
339
+ "snmp": {"routers", "switches", "multilayer switches", "access points", "servers"},
340
+ "netflow": {"routers"},
341
+ "span": {"switches", "multilayer switches"},
342
+ "qos": {"routers", "switches", "multilayer switches"},
343
+ "port_security": {"switches", "multilayer switches"},
344
+ "asa_acl_nat": {"security devices"},
345
+ "asa_service_policy": {"security devices"},
346
+ "clientless_vpn": {"security devices"},
347
+ "cbac": {"routers", "security devices"},
348
+ "zfw": {"routers", "security devices"},
349
+ "sniffer": {"end devices", "pt-specific edge/utility devices"},
350
+ "wlc": {"access points"},
351
+ "wpa_enterprise": {"access points", "home/wireless routers", "servers"},
352
+ "wep": {"access points", "home/wireless routers"},
353
+ "guest_wifi": {"home/wireless routers", "access points"},
354
+ "beamforming": {"home/wireless routers"},
355
+ "meraki": {"security devices", "home/wireless routers", "access points"},
356
+ "cellular_5g": {"wan/cloud/dsl/cable devices", "end devices"},
357
+ "bluetooth": {"iot devices", "end devices"},
358
+ "network_controller": {"pt-specific edge/utility devices", "routers", "switches"},
359
+ "python_programming": {"end devices", "pt-specific edge/utility devices"},
360
+ "javascript_programming": {"end devices", "pt-specific edge/utility devices"},
361
+ "blockly_programming": {"end devices", "pt-specific edge/utility devices"},
362
+ "tcp_udp_app": {"end devices", "servers"},
363
+ "vm_iox": {"routers", "pt-specific edge/utility devices"},
364
+ "voip": {"end devices", "switches", "routers"},
365
+ "ip_phone": {"end devices"},
366
+ "call_manager": {"routers", "servers", "end devices", "switches"},
367
+ "linksys_voice": {"home/wireless routers", "end devices"},
368
+ "mqtt": {"iot devices", "servers", "home/wireless routers", "pt-specific edge/utility devices"},
369
+ "real_http": {"iot devices", "servers", "pt-specific edge/utility devices"},
370
+ "real_websocket": {"iot devices", "servers", "pt-specific edge/utility devices"},
371
+ "visual_scripting": {"iot devices"},
372
+ "ptp": {"switches", "multilayer switches", "routers"},
373
+ "profinet": {"iot devices", "routers", "switches"},
374
+ "l2nat": {"switches", "multilayer switches"},
375
+ "cyberobserver": {"servers", "switches", "routers"},
376
+ "industrial_firewall": {"security devices", "routers"},
377
+ "coaxial": {"pt-specific edge/utility devices"},
378
+ "cable_dsl": {"wan/cloud/dsl/cable devices"},
379
+ "central_office": {"pt-specific edge/utility devices"},
380
+ "cell_tower": {"pt-specific edge/utility devices", "wan/cloud/dsl/cable devices"},
381
+ "power_distribution": {"pt-specific edge/utility devices"},
382
+ "hot_swappable": {"multilayer switches", "pt-specific edge/utility devices"},
383
+ "ios_license": {"routers", "switches", "multilayer switches"},
384
+ }
385
+
386
+
387
+ @dataclass
388
+ class CapabilityMatrixEntry:
389
+ device_family: str
390
+ capability: str
391
+ maturity_level: str
392
+ accepted_donors: list[str]
393
+ known_limitations: list[str]
394
+
395
+
396
+ @dataclass
397
+ class CoverageGapReport:
398
+ requested_capabilities: list[str]
399
+ supported_capabilities: list[str]
400
+ unsupported_capabilities: list[str]
401
+ requested_device_families: list[str]
402
+ unsupported_device_families: list[str]
403
+ requires_curated_donor: list[str]
404
+ requires_manual_acceptance: list[str]
405
+ capability_statuses: list[dict[str, object]]
406
+ capability_parity: list[dict[str, object]]
407
+ scenario_family: str | None = None
408
+ scenario_generate_readiness: dict[str, object] = field(default_factory=dict)
409
+ recommended_next_actions: list[str] = field(default_factory=list)
410
+
411
+ @property
412
+ def critical_capability_parity(self) -> list[dict[str, object]]:
413
+ critical = set(self.scenario_generate_readiness.get("critical_capabilities") or [])
414
+ if not critical:
415
+ return []
416
+ return [entry for entry in self.capability_parity if entry.get("capability") in critical]
417
+
418
+ @property
419
+ def critical_parity_supported_count(self) -> int:
420
+ return sum(1 for entry in self.critical_capability_parity if entry.get("inventory_supported") or entry.get("edit_supported"))
421
+
422
+ @property
423
+ def critical_parity_generate_ready_count(self) -> int:
424
+ return sum(1 for entry in self.critical_capability_parity if entry.get("generate_supported"))
425
+
426
+ @property
427
+ def critical_parity_acceptance_verified_count(self) -> int:
428
+ return sum(1 for entry in self.critical_capability_parity if entry.get("acceptance_verified"))
429
+
430
+ @property
431
+ def critical_parity_donor_backed_ready_count(self) -> int:
432
+ return sum(1 for entry in self.critical_capability_parity if entry.get("donor_backed_ready"))
433
+
434
+ @property
435
+ def critical_parity_mismatch_count(self) -> int:
436
+ return sum(1 for entry in self.critical_capability_parity if entry.get("generate_mismatch_reason"))
437
+
438
+
439
+ @dataclass
440
+ class BlueprintPlan:
441
+ requested_devices: list[dict[str, object]]
442
+ requested_links: list[dict[str, object]]
443
+ required_capabilities: list[str]
444
+ required_donor_capacity: dict[str, int]
445
+ required_link_pairs: list[str]
446
+
447
+
448
+ @dataclass
449
+ class DonorGraphFit:
450
+ matched_pairs: list[str]
451
+ missing_pairs: list[str]
452
+ port_media_conflicts: list[str]
453
+ fit_score: int
454
+ layout_reuse_score: int
455
+
456
+
457
+ def _device_family_for_type(device_type: str) -> str:
458
+ return DEVICE_FAMILY_MAP.get(device_type, "pt-specific edge/utility devices")
459
+
460
+
461
+ def _sample_maturity(sample: SampleDescriptor) -> str:
462
+ if sample.origin == "cisco-local" and sample.prototype_eligible and sample.version.startswith("9."):
463
+ structural = "acceptance-verified"
464
+ elif sample.origin in {"compat-donor", "external-curated"} and sample.donor_eligible and sample.prototype_eligible:
465
+ structural = "safe-open-generate-supported"
466
+ elif sample.capability_tags:
467
+ structural = "config-mutation-supported"
468
+ elif sample.devices:
469
+ structural = "inventory-supported"
470
+ else:
471
+ structural = "reference-known"
472
+ declared = sample.apply_safety_level if sample.apply_safety_level in MATURITY_RANK else "reference-known"
473
+ if MATURITY_RANK[declared] > MATURITY_RANK[structural]:
474
+ return declared
475
+ return structural
476
+
477
+
478
+ def _merge_limitations(existing: list[str], new_items: list[str]) -> list[str]:
479
+ merged = list(existing)
480
+ for item in new_items:
481
+ if item and item not in merged:
482
+ merged.append(item)
483
+ return merged
484
+
485
+
486
+ def _expanded_sample_capabilities(sample: SampleDescriptor, device_families: list[str]) -> list[str]:
487
+ capabilities = set(sample.capability_tags)
488
+ service_capability_map = {
489
+ "dhcp": {"dhcp", "server_dhcp"},
490
+ "dns": {"dns", "server_dns"},
491
+ "http": {"server_http"},
492
+ "https": {"server_https"},
493
+ "ftp": {"server_ftp"},
494
+ "tftp": {"server_tftp"},
495
+ "email": {"server_email"},
496
+ "syslog": {"server_syslog"},
497
+ "aaa": {"server_aaa"},
498
+ "ntp": {"ntp"},
499
+ }
500
+ for service in sample.service_support:
501
+ capabilities.add(service)
502
+ capabilities.update(service_capability_map.get(service, set()))
503
+ families = set(device_families)
504
+ topology_tags = set(sample.topology_tags)
505
+ normalized_counts = sample.normalized_device_counts()
506
+ iot_roles = set(sample.iot_roles)
507
+
508
+ if "vlan" in capabilities and families & {"switches", "multilayer switches"}:
509
+ capabilities.update({"trunk", "access_port"})
510
+ if (
511
+ "router_on_a_stick" in capabilities
512
+ or "router_on_a_stick" in topology_tags
513
+ or ("vlan" in capabilities and {"routers", "switches"} <= families)
514
+ ):
515
+ capabilities.add("router_on_a_stick")
516
+ if "management_vlan" in capabilities:
517
+ capabilities.add("telnet")
518
+ if "telnet" in capabilities and families & {"switches", "multilayer switches"}:
519
+ capabilities.add("management_vlan")
520
+ if normalized_counts.get("Printer", 0) > 0 or ("end devices" in families and "host_server" in capabilities):
521
+ capabilities.add("printer")
522
+ if "end devices" in families:
523
+ capabilities.add("end_device_mutation")
524
+ if sample.wireless_mode_tags and {"access points", "home/wireless routers"} & families and "wireless_ap" in capabilities:
525
+ capabilities.add("wireless_mutation")
526
+ capabilities.add("wireless_client")
527
+ capabilities.add("wireless_client_association")
528
+ if iot_roles:
529
+ capabilities.add("iot")
530
+ if "thing" in iot_roles and ("server" in iot_roles or "gateway" in iot_roles):
531
+ capabilities.add("iot_registration")
532
+ if "thing" in iot_roles and ("gateway" in iot_roles or "server" in iot_roles):
533
+ capabilities.add("iot_control")
534
+ if sample.link_count > 0 and families & {"routers", "switches", "multilayer switches", "servers", "end devices", "access points"}:
535
+ capabilities.add("verification")
536
+ if "multilayer switches" in families:
537
+ capabilities.add("multilayer_switching")
538
+ if "wan/cloud/dsl/cable devices" in families:
539
+ capabilities.add("wan")
540
+ if "security devices" in families:
541
+ capabilities.add("security_edge")
542
+ if "vpn" in capabilities:
543
+ capabilities.update({"ipsec", "gre"})
544
+ return sorted(capabilities)
545
+
546
+
547
+ def build_capability_matrix(samples: list[SampleDescriptor]) -> list[CapabilityMatrixEntry]:
548
+ entries: dict[tuple[str, str], CapabilityMatrixEntry] = {}
549
+ for sample in samples:
550
+ maturity = _sample_maturity(sample)
551
+ device_families = sorted({_device_family_for_type(device.get("type", "")) for device in sample.devices if device.get("type")})
552
+ capabilities = _expanded_sample_capabilities(sample, device_families)
553
+ limitations: list[str] = []
554
+ if not sample.donor_eligible:
555
+ limitations.append("not donor-eligible")
556
+ if not sample.prototype_eligible:
557
+ limitations.append("reference-only")
558
+ for family in device_families or ["pt-specific edge/utility devices"]:
559
+ known_family_limitations = DEFAULT_FAMILY_LIMITATIONS.get(family, [])
560
+ for capability in capabilities or ["inventory"]:
561
+ key = (family, capability)
562
+ donor_label = sample.relative_path
563
+ if key not in entries:
564
+ entries[key] = CapabilityMatrixEntry(
565
+ device_family=family,
566
+ capability=capability,
567
+ maturity_level=maturity,
568
+ accepted_donors=[donor_label],
569
+ known_limitations=_merge_limitations([], [*known_family_limitations, *limitations]),
570
+ )
571
+ continue
572
+ current = entries[key]
573
+ if MATURITY_RANK[maturity] > MATURITY_RANK[current.maturity_level]:
574
+ current.maturity_level = maturity
575
+ if donor_label not in current.accepted_donors:
576
+ current.accepted_donors.append(donor_label)
577
+ current.known_limitations = _merge_limitations(current.known_limitations, [*known_family_limitations, *limitations])
578
+ return sorted(entries.values(), key=lambda item: (item.device_family, item.capability))
579
+
580
+
581
+ def _requested_device_families(plan: IntentPlan) -> list[str]:
582
+ families = {_device_family_for_type(device_type) for device_type, count in plan.device_requirements.items() if count}
583
+ return sorted(families)
584
+
585
+
586
+ def _scenario_family_for_plan(plan: IntentPlan, requested_families: list[str]) -> str | None:
587
+ capabilities = set(plan.capabilities)
588
+ requested_services = {str(service) for service in plan.service_requirements.get("services", []) if service}
589
+ requested_family_set = set(requested_families)
590
+ prompt_lower = str(plan.prompt or "").lower()
591
+ campus_capabilities = {"vlan", "router_on_a_stick", "management_vlan", "telnet", "acl", "nat", "pat", "trunk", "access_port"}
592
+ service_capabilities = {"server_dns", "server_dhcp", "server_http", "server_https", "server_ftp", "server_tftp", "server_email", "server_syslog", "server_aaa"}
593
+ explicit_campus_style = plan.network_style in {"campus", "department_lan"}
594
+ explicit_service_style = plan.network_style == "service_heavy"
595
+ explicit_home_style = plan.network_style in {"home_iot", "smart_home"}
596
+ explicit_home_gateway_prompt = any(
597
+ token in prompt_lower
598
+ for token in ["home gateway", "homegateway", "smart home", "iot "]
599
+ )
600
+ campus_signal = (
601
+ explicit_campus_style
602
+ or bool(plan.department_groups)
603
+ or "campus" in prompt_lower
604
+ or "kampus" in prompt_lower
605
+ or (requested_family_set & {"switches", "multilayer switches"} and capabilities & campus_capabilities)
606
+ or len(capabilities & campus_capabilities) >= 3
607
+ )
608
+ home_iot_signal = (
609
+ explicit_home_style
610
+ or explicit_home_gateway_prompt
611
+ or plan.wireless_mode == "home_router_edge"
612
+ or capabilities & {"iot", "iot_registration", "iot_control"}
613
+ or {"iot devices", "home/wireless routers"} & requested_family_set
614
+ )
615
+ wan_signal = (
616
+ plan.network_style == "wan_security"
617
+ or capabilities & {"vpn", "ipsec", "gre", "ppp", "multilayer_switching", "security_edge", "cbac", "zfw"}
618
+ or {"wan/cloud/dsl/cable devices", "security devices"} & requested_family_set
619
+ )
620
+ ipv6_routing_signal = plan.network_style == "ipv6_routing" or capabilities & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ipv6_prefix_delegation", "ipv6_dns_aaaa", "ipv6_tunneling", "isatap", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}
621
+ ipv4_routing_management_signal = plan.network_style == "ipv4_routing_management" or capabilities & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"}
622
+ l2_resiliency_routing_signal = plan.network_style == "l2_resiliency_routing" or capabilities & {"bgp", "stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"}
623
+ l2_security_monitoring_signal = plan.network_style == "l2_security_monitoring" or capabilities & {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"}
624
+ wireless_advanced_signal = plan.network_style == "wireless_advanced" or capabilities & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"}
625
+ automation_controller_signal = plan.network_style == "automation_controller" or capabilities & {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"}
626
+ voice_collaboration_signal = plan.network_style == "voice_collaboration" or capabilities & {"voip", "ip_phone", "call_manager", "linksys_voice"}
627
+ industrial_iot_signal = plan.network_style == "industrial_iot" or capabilities & {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"}
628
+ physical_media_device_signal = plan.network_style == "physical_media_device" or capabilities & {"coaxial", "cable_dsl", "central_office", "cell_tower", "power_distribution", "hot_swappable", "ios_license"}
629
+ service_signal = (
630
+ explicit_service_style
631
+ or requested_services & {"dns", "dhcp", "http", "https", "ftp", "tftp", "email", "syslog", "aaa", "ntp"}
632
+ or capabilities & service_capabilities
633
+ or "servers" in requested_family_set
634
+ )
635
+ if industrial_iot_signal:
636
+ return "industrial_iot"
637
+ if automation_controller_signal:
638
+ return "automation_controller"
639
+ if voice_collaboration_signal:
640
+ return "voice_collaboration"
641
+ if physical_media_device_signal:
642
+ return "physical_media_device"
643
+ if wireless_advanced_signal:
644
+ return "wireless_advanced"
645
+ if wan_signal:
646
+ return "wan_security_edge"
647
+ if l2_resiliency_routing_signal:
648
+ return "l2_resiliency_routing"
649
+ if ipv4_routing_management_signal:
650
+ return "ipv4_routing_management"
651
+ if l2_security_monitoring_signal:
652
+ return "l2_security_monitoring"
653
+ if ipv6_routing_signal:
654
+ return "ipv6_routing"
655
+ if home_iot_signal:
656
+ return "home_iot"
657
+ if campus_signal:
658
+ return "campus"
659
+ if service_signal:
660
+ return "service_heavy"
661
+ return None
662
+
663
+
664
+ SCENARIO_CAPABILITY_SETS = {
665
+ "campus": {"router_on_a_stick", "trunk", "access_port", "management_vlan", "telnet", "verification", "acl", "nat"},
666
+ "service_heavy": {"server_dns", "server_dhcp", "server_http", "server_https", "server_ftp", "server_tftp", "server_email", "server_syslog", "server_aaa", "ntp"},
667
+ "home_iot": {"iot", "iot_registration", "iot_control", "wireless_ap", "wireless_mutation"},
668
+ "wan_security_edge": {"vpn", "ipsec", "gre", "ppp", "acl", "nat", "cbac", "zfw"},
669
+ "ipv6_routing": {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ipv6_prefix_delegation", "ipv6_dns_aaaa", "ipv6_tunneling", "isatap", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"},
670
+ "l2_resiliency_routing": {"bgp", "stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"},
671
+ "ipv4_routing_management": {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"},
672
+ "l2_security_monitoring": {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"},
673
+ "wireless_advanced": {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"},
674
+ "automation_controller": {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"},
675
+ "voice_collaboration": {"voip", "ip_phone", "call_manager", "linksys_voice"},
676
+ "industrial_iot": {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"},
677
+ "physical_media_device": {"coaxial", "cable_dsl", "central_office", "cell_tower", "power_distribution", "hot_swappable", "ios_license"},
678
+ }
679
+
680
+ CAPABILITY_REQUIRED_ARCHETYPES = {
681
+ "router_on_a_stick": ["campus/core"],
682
+ "management_vlan": ["campus/core"],
683
+ "telnet": ["campus/core"],
684
+ "server_dns": ["service-heavy"],
685
+ "server_dhcp": ["service-heavy"],
686
+ "server_http": ["service-heavy"],
687
+ "server_https": ["service-heavy"],
688
+ "server_ftp": ["service-heavy"],
689
+ "server_tftp": ["service-heavy"],
690
+ "server_email": ["service-heavy"],
691
+ "server_syslog": ["service-heavy"],
692
+ "server_aaa": ["service-heavy"],
693
+ "iot_registration": ["IoT/home gateway"],
694
+ "iot_control": ["IoT/home gateway"],
695
+ "wireless_mutation": ["wireless-heavy"],
696
+ "wireless_client_association": ["wireless-heavy", "IoT/home gateway"],
697
+ "vpn": ["WAN/security edge"],
698
+ "ipsec": ["WAN/security edge"],
699
+ "gre": ["WAN/security edge"],
700
+ "ppp": ["WAN/security edge"],
701
+ "multilayer_switching": ["WAN/security edge", "campus/core"],
702
+ "security_edge": ["WAN/security edge"],
703
+ "cbac": ["WAN/security edge"],
704
+ "zfw": ["WAN/security edge"],
705
+ "ipv6_slaac": ["IPv6/routing"],
706
+ "dhcpv6_stateful": ["IPv6/routing"],
707
+ "dhcpv6_stateless": ["IPv6/routing"],
708
+ "ipv6_prefix_delegation": ["IPv6/routing"],
709
+ "ipv6_dns_aaaa": ["IPv6/routing"],
710
+ "ipv6_tunneling": ["IPv6/routing"],
711
+ "isatap": ["IPv6/routing"],
712
+ "ospfv3": ["IPv6/routing"],
713
+ "eigrp_ipv6": ["IPv6/routing"],
714
+ "ripng": ["IPv6/routing"],
715
+ "hsrp": ["IPv6/routing"],
716
+ "dhcp_snooping": ["L2 security/monitoring"],
717
+ "dai": ["L2 security/monitoring"],
718
+ "dot1x": ["L2 security/monitoring"],
719
+ "lldp": ["L2 security/monitoring"],
720
+ "rep": ["L2 security/monitoring"],
721
+ "snmp": ["L2 security/monitoring"],
722
+ "netflow": ["L2 security/monitoring"],
723
+ "span": ["L2 security/monitoring"],
724
+ "qos": ["L2 security/monitoring"],
725
+ "port_security": ["L2 security/monitoring"],
726
+ "bgp": ["L2 resiliency/routing"],
727
+ "stp": ["L2 resiliency/routing"],
728
+ "rstp": ["L2 resiliency/routing"],
729
+ "etherchannel": ["L2 resiliency/routing"],
730
+ "lacp": ["L2 resiliency/routing"],
731
+ "pagp": ["L2 resiliency/routing"],
732
+ "vtp": ["L2 resiliency/routing"],
733
+ "dtp": ["L2 resiliency/routing"],
734
+ "ospfv2": ["IPv4 routing/management"],
735
+ "eigrp_ipv4": ["IPv4 routing/management"],
736
+ "ripv2": ["IPv4 routing/management"],
737
+ "static_route": ["IPv4 routing/management"],
738
+ "default_route": ["IPv4 routing/management"],
739
+ "dhcp_relay": ["IPv4 routing/management"],
740
+ "nat_static": ["IPv4 routing/management"],
741
+ "nat_dynamic": ["IPv4 routing/management"],
742
+ "pat": ["IPv4 routing/management"],
743
+ "ssh_ios": ["IPv4 routing/management"],
744
+ "ntp_ios": ["IPv4 routing/management"],
745
+ "syslog_ios": ["IPv4 routing/management"],
746
+ "wlc": ["advanced wireless"],
747
+ "wpa_enterprise": ["advanced wireless"],
748
+ "wep": ["advanced wireless"],
749
+ "guest_wifi": ["advanced wireless"],
750
+ "beamforming": ["advanced wireless"],
751
+ "meraki": ["advanced wireless"],
752
+ "cellular_5g": ["advanced wireless"],
753
+ "bluetooth": ["advanced wireless"],
754
+ "network_controller": ["automation/controller"],
755
+ "python_programming": ["automation/controller"],
756
+ "javascript_programming": ["automation/controller"],
757
+ "blockly_programming": ["automation/controller"],
758
+ "tcp_udp_app": ["automation/controller"],
759
+ "vm_iox": ["automation/controller"],
760
+ "voip": ["voice/collaboration"],
761
+ "ip_phone": ["voice/collaboration"],
762
+ "call_manager": ["voice/collaboration"],
763
+ "linksys_voice": ["voice/collaboration"],
764
+ "mqtt": ["industrial IoT"],
765
+ "real_http": ["industrial IoT"],
766
+ "real_websocket": ["industrial IoT"],
767
+ "visual_scripting": ["industrial IoT"],
768
+ "ptp": ["industrial IoT"],
769
+ "profinet": ["industrial IoT"],
770
+ "l2nat": ["industrial IoT"],
771
+ "cyberobserver": ["industrial IoT"],
772
+ "industrial_firewall": ["industrial IoT"],
773
+ }
774
+
775
+ CAPABILITY_REQUIRED_RUNTIME_FEATURES = {
776
+ "server_dns": ["workspace_validated", "server_runtime"],
777
+ "server_dhcp": ["workspace_validated", "server_runtime"],
778
+ "server_http": ["workspace_validated", "server_runtime"],
779
+ "server_https": ["workspace_validated", "server_runtime"],
780
+ "server_ftp": ["workspace_validated", "server_runtime"],
781
+ "server_tftp": ["workspace_validated", "server_runtime"],
782
+ "server_email": ["workspace_validated", "server_runtime"],
783
+ "server_syslog": ["workspace_validated", "server_runtime"],
784
+ "server_aaa": ["workspace_validated", "server_runtime"],
785
+ "wireless_mutation": ["workspace_validated", "wireless_runtime"],
786
+ "wireless_client_association": ["workspace_validated", "wireless_runtime"],
787
+ "iot_registration": ["workspace_validated", "iot_runtime"],
788
+ "iot_control": ["workspace_validated", "iot_runtime"],
789
+ "vpn": ["workspace_validated", "tunnel_runtime"],
790
+ "ipsec": ["workspace_validated", "tunnel_runtime"],
791
+ "gre": ["workspace_validated", "tunnel_runtime"],
792
+ "ppp": ["workspace_validated", "wan_runtime"],
793
+ "security_edge": ["workspace_validated", "security_runtime"],
794
+ "cbac": ["workspace_validated", "security_runtime"],
795
+ "zfw": ["workspace_validated", "security_runtime"],
796
+ "multilayer_switching": ["workspace_validated", "multilayer_runtime"],
797
+ "ipv6_slaac": ["workspace_validated", "ipv6_runtime"],
798
+ "dhcpv6_stateful": ["workspace_validated", "ipv6_runtime"],
799
+ "ospfv3": ["workspace_validated", "ipv6_runtime"],
800
+ "eigrp_ipv6": ["workspace_validated", "ipv6_runtime"],
801
+ "ripng": ["workspace_validated", "ipv6_runtime"],
802
+ "hsrp": ["workspace_validated", "ipv6_runtime"],
803
+ "wep": ["workspace_validated", "wireless_runtime"],
804
+ "wpa_enterprise": ["workspace_validated", "wireless_runtime"],
805
+ "ospfv2": ["workspace_validated", "ipv4_routing_management_runtime"],
806
+ "eigrp_ipv4": ["workspace_validated", "ipv4_routing_management_runtime"],
807
+ "ripv2": ["workspace_validated", "ipv4_routing_management_runtime"],
808
+ "static_route": ["workspace_validated", "ipv4_routing_management_runtime"],
809
+ "default_route": ["workspace_validated", "ipv4_routing_management_runtime"],
810
+ "dhcp_relay": ["workspace_validated", "ipv4_routing_management_runtime"],
811
+ "nat_static": ["workspace_validated", "ipv4_routing_management_runtime"],
812
+ "nat_dynamic": ["workspace_validated", "ipv4_routing_management_runtime"],
813
+ "pat": ["workspace_validated", "ipv4_routing_management_runtime"],
814
+ "ssh_ios": ["workspace_validated", "ipv4_routing_management_runtime"],
815
+ "ntp_ios": ["workspace_validated", "ipv4_routing_management_runtime"],
816
+ "syslog_ios": ["workspace_validated", "ipv4_routing_management_runtime"],
817
+ }
818
+
819
+
820
+ def _generate_mismatch_reason(status: dict[str, object]) -> str | None:
821
+ if bool(status.get("report_only")):
822
+ return "report_only"
823
+ if not bool(status.get("inventory_supported")):
824
+ return "unsupported"
825
+ if bool(status.get("donor_limited_generate")):
826
+ return "supported_but_donor_limited"
827
+ if bool(status.get("edit_supported")) and not bool(status.get("safe_open_generate_supported")):
828
+ return "supported_in_edit_only"
829
+ if bool(status.get("safe_open_generate_supported")) and bool(status.get("requires_curated_donor")):
830
+ return "supported_but_donor_limited"
831
+ if bool(status.get("safe_open_generate_supported")) and bool(status.get("requires_manual_acceptance")) and not bool(status.get("acceptance_verified")):
832
+ return "supported_but_acceptance_gated"
833
+ if bool(status.get("safe_open_generate_supported")):
834
+ return None
835
+ return "unsupported"
836
+
837
+
838
+ def _donor_backed_ready(status: dict[str, object]) -> bool:
839
+ return bool(status.get("donor_backed_ready"))
840
+
841
+
842
+ def _selected_sample_for_donor(
843
+ samples: list[SampleDescriptor],
844
+ selected_donor: str | None,
845
+ ) -> SampleDescriptor | None:
846
+ if not selected_donor:
847
+ return None
848
+ needle = str(selected_donor).replace("\\", "/").lower()
849
+ for sample in samples:
850
+ candidates = {
851
+ str(sample.path).replace("\\", "/").lower(),
852
+ str(sample.relative_path).replace("\\", "/").lower(),
853
+ str(sample.path).replace("\\", "/").split("/")[-1].lower(),
854
+ str(sample.relative_path).replace("\\", "/").split("/")[-1].lower(),
855
+ }
856
+ if needle in candidates:
857
+ return sample
858
+ return None
859
+
860
+
861
+ def _has_explicit_iot_registration(plan: IntentPlan) -> bool:
862
+ return any(
863
+ op.get("op") == "set_iot_registration"
864
+ and str(op.get("device", "")).strip()
865
+ and str(op.get("target", "")).strip()
866
+ for op in plan.iot_ops
867
+ )
868
+
869
+
870
+ def _has_explicit_iot_control(plan: IntentPlan) -> bool:
871
+ return any(
872
+ op.get("op") == "set_iot_rule_state"
873
+ and str(op.get("device", "")).strip()
874
+ and str(op.get("rule_name", "")).strip()
875
+ for op in plan.iot_ops
876
+ )
877
+
878
+
879
+ def _has_explicit_wireless_association(plan: IntentPlan) -> bool:
880
+ return any(
881
+ op.get("op") == "associate_wireless_client"
882
+ and str(op.get("device", "")).strip()
883
+ and str(op.get("ap", "")).strip()
884
+ and str(op.get("ssid", "")).strip()
885
+ for op in plan.wireless_ops
886
+ )
887
+
888
+
889
+ def _has_explicit_advanced_wireless_edit(plan: IntentPlan, capability: str) -> bool:
890
+ for op in plan.wireless_ops:
891
+ if op.get("op") != "set_wireless_ssid":
892
+ continue
893
+ security = str(op.get("security") or "").strip().lower()
894
+ if capability == "wep" and security == "wep" and str(op.get("device") or "").strip() and str(op.get("ssid") or "").strip():
895
+ return True
896
+ if capability == "wpa_enterprise" and security in {"wpa-enterprise", "wpa2-enterprise", "802.1x"} and str(op.get("device") or "").strip() and str(op.get("ssid") or "").strip():
897
+ return True
898
+ return False
899
+
900
+
901
+ def _has_explicit_programming_edit(plan: IntentPlan, capability: str) -> bool:
902
+ for op in plan.programming_ops:
903
+ if op.get("op") != "set_script_file_content":
904
+ continue
905
+ if not (
906
+ str(op.get("device") or "").strip()
907
+ and str(op.get("app_name") or "").strip()
908
+ and str(op.get("file_name") or "").strip()
909
+ ):
910
+ continue
911
+ file_name = str(op.get("file_name") or "").lower()
912
+ app_name = str(op.get("app_name") or "").lower()
913
+ content = str(op.get("content") or "").lower()
914
+ if capability == "python_programming" and (file_name.endswith(".py") or "python" in app_name):
915
+ return True
916
+ if capability == "javascript_programming" and (file_name.endswith(".js") or "javascript" in app_name):
917
+ return True
918
+ script_text = " ".join([file_name, app_name, content])
919
+ if capability == "tcp_udp_app" and ("tcp" in script_text or "udp" in script_text):
920
+ return True
921
+ if capability == "real_http" and ("realhttp" in content or "real http" in app_name):
922
+ return True
923
+ if capability == "real_websocket" and ("realws" in content or "websocket" in content or "websocket" in app_name):
924
+ return True
925
+ return False
926
+
927
+
928
+ def _has_explicit_l2_security_edit(plan: IntentPlan, capability: str) -> bool:
929
+ expected_ops = {
930
+ "dot1x": {"set_dot1x"},
931
+ "qos": {"set_qos_policy"},
932
+ }.get(capability, set())
933
+ if not expected_ops:
934
+ return False
935
+ for op in plan.switch_ops:
936
+ if str(op.get("op")) not in expected_ops:
937
+ continue
938
+ if not str(op.get("device") or "").strip():
939
+ continue
940
+ if capability == "dot1x":
941
+ return bool(str(op.get("interface") or "").strip() and str(op.get("mode") or "").strip())
942
+ if capability == "qos":
943
+ return bool(
944
+ str(op.get("interface") or "").strip()
945
+ and str(op.get("class_map") or "").strip()
946
+ and str(op.get("policy_map") or "").strip()
947
+ and str(op.get("direction") or "").strip()
948
+ )
949
+ return False
950
+
951
+
952
+ def _has_explicit_l2_resiliency_edit(plan: IntentPlan, capability: str) -> bool:
953
+ if capability == "bgp":
954
+ return any(
955
+ op.get("op") == "set_bgp_neighbor"
956
+ and str(op.get("device") or "").strip()
957
+ and str(op.get("neighbor") or "").strip()
958
+ and str(op.get("remote_as") or "").strip()
959
+ for op in plan.router_ops
960
+ )
961
+ for op in plan.switch_ops:
962
+ if not str(op.get("device") or "").strip():
963
+ continue
964
+ op_name = str(op.get("op"))
965
+ if capability == "stp" and op_name == "set_stp":
966
+ return bool(str(op.get("mode") or "").strip())
967
+ if capability == "rstp" and op_name == "set_stp":
968
+ return bool(re.search(r"(?:rapid|rstp)", str(op.get("mode") or ""), flags=re.IGNORECASE))
969
+ if capability == "etherchannel" and op_name == "set_etherchannel":
970
+ return bool(op.get("interfaces"))
971
+ if capability == "lacp" and op_name == "set_etherchannel":
972
+ if str(op.get("mode") or "").lower() in {"active", "passive"} and bool(op.get("interfaces")):
973
+ return True
974
+ continue
975
+ if capability == "pagp" and op_name == "set_etherchannel":
976
+ if str(op.get("mode") or "").lower() in {"desirable", "auto"} and bool(op.get("interfaces")):
977
+ return True
978
+ continue
979
+ if capability == "vtp" and op_name == "set_vtp":
980
+ return bool(str(op.get("domain") or "").strip() and str(op.get("mode") or "").strip())
981
+ if capability == "dtp" and op_name == "set_dtp":
982
+ return bool(str(op.get("interface") or "").strip() and str(op.get("mode") or "").strip())
983
+ return False
984
+
985
+
986
+ def _has_explicit_ipv4_routing_management_edit(plan: IntentPlan, capability: str) -> bool:
987
+ operation_map = {
988
+ "ospfv2": {"set_ospfv2_network"},
989
+ "eigrp_ipv4": {"set_eigrp_ipv4_network"},
990
+ "ripv2": {"set_ripv2_network"},
991
+ "static_route": {"set_static_route"},
992
+ "default_route": {"set_static_route"},
993
+ "dhcp_relay": {"set_dhcp_relay"},
994
+ "nat_static": {"set_nat_static"},
995
+ "nat_dynamic": {"set_nat_interface"},
996
+ "pat": {"set_pat_overload"},
997
+ "ssh_ios": {"set_ssh_ios"},
998
+ "ntp_ios": {"set_ntp_server"},
999
+ "syslog_ios": {"set_syslog_server"},
1000
+ }
1001
+ expected_ops = operation_map.get(capability, set())
1002
+ if not expected_ops:
1003
+ return False
1004
+ for op in plan.router_ops:
1005
+ if str(op.get("op")) not in expected_ops:
1006
+ continue
1007
+ if not str(op.get("device") or "").strip():
1008
+ continue
1009
+ if capability == "default_route":
1010
+ return op.get("network") == "0.0.0.0" and op.get("prefix") == 0
1011
+ if capability == "nat_dynamic":
1012
+ return str(op.get("role") or "").lower() in {"inside", "outside"} and str(op.get("interface") or "").strip()
1013
+ return True
1014
+ return False
1015
+
1016
+
1017
+ def _has_explicit_wan_security_edit(plan: IntentPlan, capability: str) -> bool:
1018
+ expected_ops = {
1019
+ "gre": {"set_gre_tunnel"},
1020
+ "ppp": {"set_ppp_interface"},
1021
+ "ipsec": {"set_ipsec_transform_set", "set_crypto_map"},
1022
+ "vpn": {"set_crypto_map"},
1023
+ "cbac": {"set_cbac_inspect"},
1024
+ "zfw": {"set_zfw_zone_interface", "set_zfw_zone_pair", "set_zfw_policy"},
1025
+ }.get(capability, set())
1026
+ if not expected_ops:
1027
+ return False
1028
+ for op in plan.router_ops:
1029
+ if str(op.get("op")) not in expected_ops:
1030
+ continue
1031
+ if not str(op.get("device") or "").strip():
1032
+ continue
1033
+ if capability == "gre":
1034
+ return bool(str(op.get("interface") or "").strip() and str(op.get("source") or "").strip() and str(op.get("destination") or "").strip())
1035
+ if capability == "ppp":
1036
+ return bool(str(op.get("interface") or "").strip())
1037
+ if capability == "ipsec":
1038
+ if op.get("op") == "set_ipsec_transform_set":
1039
+ return bool(str(op.get("name") or "").strip() and str(op.get("encryption") or "").strip() and str(op.get("integrity") or "").strip())
1040
+ return bool(str(op.get("map_name") or "").strip() and str(op.get("transform_set") or "").strip())
1041
+ if capability == "vpn":
1042
+ return bool(str(op.get("map_name") or "").strip() and str(op.get("peer") or "").strip())
1043
+ if capability == "cbac":
1044
+ return bool(str(op.get("name") or "").strip() and str(op.get("protocol") or "").strip() and str(op.get("interface") or "").strip())
1045
+ if capability == "zfw":
1046
+ if op.get("op") == "set_zfw_zone_interface":
1047
+ return bool(str(op.get("zone") or "").strip() and str(op.get("interface") or "").strip())
1048
+ if op.get("op") == "set_zfw_zone_pair":
1049
+ return bool(str(op.get("pair_name") or "").strip() and str(op.get("source") or "").strip() and str(op.get("destination") or "").strip())
1050
+ return bool(str(op.get("class_map") or "").strip() and str(op.get("policy_map") or "").strip())
1051
+ return False
1052
+
1053
+
1054
+ def _has_explicit_wan_security_intent(plan: IntentPlan) -> bool:
1055
+ capabilities = set(plan.capabilities)
1056
+ prompt_lower = str(plan.prompt or "").lower()
1057
+ return (
1058
+ plan.network_style == "wan_security"
1059
+ or bool(capabilities & {"vpn", "ipsec", "gre", "ppp", "security_edge", "multilayer_switching"})
1060
+ or any(token in prompt_lower for token in ["site-to-site", "firewall", "asa", "cloud", "multilayer"])
1061
+ )
1062
+
1063
+
1064
+ def _has_explicit_voice_edit(plan: IntentPlan, capability: str) -> bool:
1065
+ op_names = {str(op.get("op")) for op in plan.router_ops}
1066
+ if capability == "voip":
1067
+ return bool(op_names & {"set_telephony_service", "set_ephone_dn", "set_ephone", "set_dial_peer_voice"})
1068
+ if capability == "call_manager":
1069
+ return bool(op_names & {"set_telephony_service", "set_ephone_dn", "set_ephone"})
1070
+ if capability == "ip_phone":
1071
+ return bool(op_names & {"set_ephone_dn", "set_ephone"})
1072
+ return False
1073
+
1074
+
1075
+ def _has_explicit_ipv6_operation(plan: IntentPlan, capability: str) -> bool:
1076
+ operation_map = {
1077
+ "ipv6_slaac": {"enable_ipv6_unicast_routing", "set_ipv6_address", "set_ipv6_slaac"},
1078
+ "dhcpv6_stateful": {"set_dhcpv6_pool"},
1079
+ "ospfv3": {"set_ospfv3_interface"},
1080
+ "eigrp_ipv6": {"set_eigrp_ipv6_interface"},
1081
+ "ripng": {"set_ripng_interface"},
1082
+ "hsrp": {"set_hsrp_ipv6"},
1083
+ }
1084
+ expected_ops = operation_map.get(capability, set())
1085
+ return any(str(op.get("op")) in expected_ops for op in plan.router_ops)
1086
+
1087
+
1088
+ def _selected_donor_capability_override(
1089
+ plan: IntentPlan,
1090
+ capability: str,
1091
+ selected_sample: SampleDescriptor | None,
1092
+ ) -> bool:
1093
+ if selected_sample is None:
1094
+ return False
1095
+ device_families = sorted({_device_family_for_type(device.get("type", "")) for device in selected_sample.devices if device.get("type")})
1096
+ sample_capabilities = set(_expanded_sample_capabilities(selected_sample, device_families))
1097
+ sample_capabilities.update(str(item) for item in selected_sample.validated_edit_capabilities if str(item).strip())
1098
+ archetypes = {str(item) for item in selected_sample.archetype_tags if str(item).strip()}
1099
+ runtime_features = {str(item) for item in selected_sample.runtime_features if str(item).strip()}
1100
+
1101
+ if capability == "iot":
1102
+ return (
1103
+ bool(archetypes & {"IoT/home gateway"})
1104
+ and bool(sample_capabilities & {"iot", "iot_registration", "iot_control"})
1105
+ and bool(runtime_features & {"iot_runtime"})
1106
+ and (_has_explicit_iot_registration(plan) or _has_explicit_iot_control(plan))
1107
+ )
1108
+ if capability == "iot_registration":
1109
+ return (
1110
+ "IoT/home gateway" in archetypes
1111
+ and "iot_registration" in sample_capabilities
1112
+ and "iot_runtime" in runtime_features
1113
+ and _has_explicit_iot_registration(plan)
1114
+ )
1115
+ if capability == "iot_control":
1116
+ return (
1117
+ "IoT/home gateway" in archetypes
1118
+ and "iot_control" in sample_capabilities
1119
+ and "iot_runtime" in runtime_features
1120
+ and _has_explicit_iot_control(plan)
1121
+ )
1122
+ if capability == "wireless_client_association":
1123
+ return (
1124
+ bool(archetypes & {"IoT/home gateway", "wireless-heavy"})
1125
+ and "wireless_client_association" in sample_capabilities
1126
+ and "wireless_runtime" in runtime_features
1127
+ and _has_explicit_wireless_association(plan)
1128
+ )
1129
+ wan_runtime_by_capability = {
1130
+ "vpn": {"tunnel_runtime", "security_runtime", "wan_runtime"},
1131
+ "ipsec": {"tunnel_runtime", "security_runtime"},
1132
+ "gre": {"tunnel_runtime", "wan_runtime"},
1133
+ "ppp": {"wan_runtime"},
1134
+ "security_edge": {"security_runtime"},
1135
+ "multilayer_switching": {"multilayer_runtime"},
1136
+ }
1137
+ if capability in wan_runtime_by_capability:
1138
+ has_acceptance_backed_wan_generate = (
1139
+ bool(selected_sample.acceptance_fixtures)
1140
+ and selected_sample.apply_safety_level in {"acceptance-verified", "safe-open-generate-supported"}
1141
+ )
1142
+ return (
1143
+ "WAN/security edge" in archetypes
1144
+ and capability in sample_capabilities
1145
+ and bool(runtime_features & wan_runtime_by_capability[capability])
1146
+ and has_acceptance_backed_wan_generate
1147
+ and _has_explicit_wan_security_intent(plan)
1148
+ )
1149
+ if capability in IPV6_DONOR_BACKED_CAPABILITIES:
1150
+ return (
1151
+ "IPv6/routing" in archetypes
1152
+ and capability in sample_capabilities
1153
+ and "ipv6_runtime" in runtime_features
1154
+ and _has_explicit_ipv6_operation(plan, capability)
1155
+ )
1156
+ return False
1157
+
1158
+
1159
+ def _best_maturity_level(status: dict[str, object]) -> str:
1160
+ level = str(status.get("best_maturity_level") or "").strip()
1161
+ if level in MATURITY_RANK:
1162
+ return level
1163
+ if bool(status.get("acceptance_verified")):
1164
+ return "acceptance-verified"
1165
+ if bool(status.get("safe_open_generate_supported")):
1166
+ return "safe-open-generate-supported"
1167
+ if bool(status.get("config_mutation_supported")):
1168
+ return "config-mutation-supported"
1169
+ if bool(status.get("edit_supported")):
1170
+ return "edit-supported"
1171
+ if bool(status.get("inventory_supported")):
1172
+ return "inventory-supported"
1173
+ return "reference-known"
1174
+
1175
+
1176
+ def _recommended_next_action_for_capability(capability: str, mismatch_reason: str | None) -> str:
1177
+ if mismatch_reason == "report_only":
1178
+ return f"Keep {capability} in atlas/report mode until donor-backed edit or generate evidence exists."
1179
+ if capability in {"vpn", "ipsec", "gre", "ppp", "security_edge", "multilayer_switching", "cbac", "zfw"}:
1180
+ if mismatch_reason == "supported_in_edit_only":
1181
+ return f"Use explicit WAN/security edit commands for {capability}; strict prompt generate still needs an acceptance-backed WAN/security donor."
1182
+ if mismatch_reason == "supported_but_donor_limited":
1183
+ return f"Provide a WAN/security edge donor with reusable ASA/cloud/serial or tunnel skeleton for {capability}."
1184
+ if mismatch_reason == "supported_but_acceptance_gated":
1185
+ return f"Keep {capability} in report/selection flow until a WAN/security edge donor is acceptance-backed."
1186
+ return f"Prefer a WAN/security edge donor with explicit {capability} evidence before strict generate."
1187
+ if capability in IPV6_DONOR_BACKED_CAPABILITIES:
1188
+ if mismatch_reason == "supported_in_edit_only":
1189
+ return f"Use explicit edit commands for {capability}; strict prompt generate still needs a selected IPv6/routing donor."
1190
+ if mismatch_reason == "supported_but_donor_limited":
1191
+ return f"Provide an IPv6/routing donor with reusable IPv6 interface and routing skeleton for {capability}."
1192
+ if mismatch_reason == "supported_but_acceptance_gated":
1193
+ return f"Keep {capability} in report/edit flow until an IPv6/routing donor is acceptance-backed."
1194
+ return f"Prefer an IPv6/routing donor and explicit router/interface targets before strict generate for {capability}."
1195
+ if capability in L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES:
1196
+ if mismatch_reason == "supported_in_edit_only":
1197
+ return f"Use explicit edit commands for {capability}; strict prompt generate still needs an acceptance-backed L2 security/monitoring donor."
1198
+ if mismatch_reason == "supported_but_donor_limited":
1199
+ return f"Provide an L2 security/monitoring donor with reusable switch/router monitoring skeleton for {capability}."
1200
+ return f"Keep {capability} constrained to edit/inventory proof until an L2 security/monitoring donor is acceptance-backed."
1201
+ if capability in WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES:
1202
+ if mismatch_reason == "supported_in_edit_only":
1203
+ return f"Use explicit wireless edit commands for {capability}; strict prompt generate still needs an acceptance-backed advanced wireless donor."
1204
+ if mismatch_reason == "supported_but_donor_limited":
1205
+ return f"Provide an advanced wireless donor with reusable AP/router/WLC skeleton for {capability}."
1206
+ return f"Keep {capability} constrained to explicit wireless edit proof until an advanced wireless donor is acceptance-backed."
1207
+ if capability in VOICE_COLLABORATION_EDIT_PROVEN_CAPABILITIES:
1208
+ if mismatch_reason == "supported_in_edit_only":
1209
+ return f"Use explicit voice IOS edit commands for {capability}; strict prompt generate still needs an acceptance-backed voice/collaboration donor."
1210
+ if mismatch_reason == "supported_but_donor_limited":
1211
+ return f"Provide a voice/collaboration donor with reusable telephony-service, ephone, or dial-peer skeleton for {capability}."
1212
+ return f"Keep {capability} constrained to voice inventory/edit proof until a voice/collaboration donor is acceptance-backed."
1213
+ if capability in AUTOMATION_CONTROLLER_EDIT_PROVEN_CAPABILITIES:
1214
+ if mismatch_reason == "supported_in_edit_only":
1215
+ return f"Use explicit script-file edit commands for {capability}; strict prompt generate still needs an acceptance-backed automation/controller donor."
1216
+ if mismatch_reason == "supported_but_donor_limited":
1217
+ return f"Provide an automation/controller donor with reusable existing app/file script surfaces for {capability}."
1218
+ return f"Keep {capability} constrained to automation inventory/edit proof until an automation/controller donor is acceptance-backed."
1219
+ if capability == "iot_registration":
1220
+ if mismatch_reason == "supported_but_acceptance_gated":
1221
+ return "Use an IoT/home gateway donor and explicitly name the thing plus gateway/server target before strict generate."
1222
+ return "Prefer an IoT/home gateway donor and explicitly name the thing plus gateway/server target."
1223
+ if capability == "iot_control":
1224
+ if mismatch_reason == "supported_but_acceptance_gated":
1225
+ return "Use a donor that already contains IoT rules and explicitly name the rule-bearing device before strict generate."
1226
+ return "Prefer an IoT/home gateway donor with existing rule/control structure before strict generate."
1227
+ if capability == "wireless_client_association":
1228
+ if mismatch_reason == "supported_but_acceptance_gated":
1229
+ return "Use a donor-backed wireless prompt that explicitly names the client, AP/router, and SSID before strict generate."
1230
+ return "Prefer a wireless-heavy or IoT/home gateway donor and explicitly name the client, AP/router, and SSID."
1231
+ if mismatch_reason == "supported_in_edit_only":
1232
+ return f"Use donor-backed edit flow for {capability} until safe-open generate coverage is raised."
1233
+ if mismatch_reason == "supported_but_donor_limited":
1234
+ return f"Provide or curate a donor whose skeleton explicitly covers {capability}."
1235
+ if mismatch_reason == "supported_but_acceptance_gated":
1236
+ return f"Keep {capability} in explain/edit flow until acceptance evidence is promoted."
1237
+ return f"Import or curate donor coverage for {capability} before strict generate."
1238
+
1239
+
1240
+ # Capabilities the planner can configure itself, verified by the corpus rather
1241
+ # than by what a donor happens to demonstrate. Coverage for these is satisfied by
1242
+ # construction: if the operations are emitted and the lab opens, the donor's own
1243
+ # feature list is beside the point.
1244
+ EMITTABLE_CAPABILITIES = {
1245
+ "vlan", "trunk", "access_port", "router_on_a_stick",
1246
+ "dhcp_pool", "router_dhcp", "dhcp_snooping", "dai",
1247
+ "ospf", "ospfv2", "ospf_multiarea", "eigrp", "eigrp_ipv4", "rip", "ripv2",
1248
+ "static_route", "default_route",
1249
+ "acl", "nat", "nat_static", "nat_dynamic", "pat",
1250
+ "stp", "rstp", "etherchannel", "lacp", "pagp", "port_security",
1251
+ "hsrp", "ipv6", "ipv6_slaac", "ospfv3",
1252
+ "telnet", "ssh_ios", "management_vlan",
1253
+ "ntp", "ntp_ios", "syslog_ios", "snmp",
1254
+ "server_dns", "server_http", "server_ftp", "server_tftp", "server_aaa",
1255
+ "dns", "voip", "ip_phone", "call_manager",
1256
+ "wireless_ap", "wireless_client",
1257
+ "ppp", "gre", "vpn", "ipsec",
1258
+ }
1259
+
1260
+
1261
+ def _scenario_generate_readiness(
1262
+ scenario_family: str | None,
1263
+ capability_statuses: list[dict[str, object]],
1264
+ unsupported_capabilities: list[str],
1265
+ requires_curated_donor: list[str],
1266
+ requires_manual_acceptance: list[str],
1267
+ ) -> dict[str, object]:
1268
+ if not scenario_family:
1269
+ return {
1270
+ "family": None,
1271
+ "status": "not_classified",
1272
+ "critical_capabilities": [],
1273
+ "missing_critical_capabilities": [],
1274
+ "reasons": [],
1275
+ }
1276
+ critical_set = SCENARIO_CAPABILITY_SETS.get(scenario_family, set())
1277
+ status_by_capability = {str(item.get("capability")): item for item in capability_statuses}
1278
+ relevant_capabilities = [cap for cap in status_by_capability if cap in critical_set]
1279
+ # A capability the planner emits operations for is covered by construction,
1280
+ # whatever the sample catalogue says about the donor. `gre` was refused as
1281
+ # "missing critical capability coverage: vpn" while a GRE lab built through
1282
+ # the same path carried `interface Tunnel0` with its source and destination
1283
+ # and opened in Packet Tracer.
1284
+ missing_critical = [
1285
+ cap
1286
+ for cap in unsupported_capabilities
1287
+ if cap in critical_set and cap not in EMITTABLE_CAPABILITIES
1288
+ ]
1289
+ donor_limited_critical = [cap for cap in requires_curated_donor if cap in critical_set]
1290
+ gated_critical = [cap for cap in requires_manual_acceptance if cap in critical_set and cap not in donor_limited_critical]
1291
+ reasons: list[str] = []
1292
+ if missing_critical:
1293
+ reasons.append(f"missing critical capability coverage: {', '.join(missing_critical)}")
1294
+ if donor_limited_critical:
1295
+ reasons.append(f"critical capabilities depend on donor-limited safe-open coverage: {', '.join(donor_limited_critical)}")
1296
+ if gated_critical:
1297
+ reasons.append(f"critical capabilities remain acceptance-gated: {', '.join(gated_critical)}")
1298
+ if missing_critical:
1299
+ status = "unsupported"
1300
+ elif donor_limited_critical:
1301
+ status = "donor_limited"
1302
+ elif gated_critical:
1303
+ status = "acceptance_gated"
1304
+ elif relevant_capabilities:
1305
+ status = "ready"
1306
+ else:
1307
+ status = "partial"
1308
+ reasons.append("scenario family is recognized, but no critical capability set was requested explicitly.")
1309
+ return {
1310
+ "family": scenario_family,
1311
+ "status": status,
1312
+ "critical_capabilities": relevant_capabilities,
1313
+ "missing_critical_capabilities": missing_critical,
1314
+ "reasons": reasons,
1315
+ }
1316
+
1317
+
1318
+ def _provider_families_for_capability(capability: str, requested_families: list[str]) -> set[str]:
1319
+ mapped = CAPABILITY_PROVIDER_FAMILIES.get(capability)
1320
+ if mapped:
1321
+ return set(mapped)
1322
+ return set(requested_families)
1323
+
1324
+
1325
+ def _normalize_media_name(value: str) -> str:
1326
+ media = value.strip().lower().replace("_", "-").replace(" ", "-")
1327
+ aliases = {
1328
+ "estraightthrough": "straight-through",
1329
+ "copper-straight-through": "straight-through",
1330
+ "straight-through": "straight-through",
1331
+ "ecrossover": "crossover",
1332
+ "copper-crossover": "crossover",
1333
+ "crossover": "crossover",
1334
+ "fiber": "fiber",
1335
+ "efiber": "fiber",
1336
+ "serial": "serial",
1337
+ "eserialdce": "serial-dce",
1338
+ "serial-dce": "serial-dce",
1339
+ "eserialdte": "serial-dte",
1340
+ "serial-dte": "serial-dte",
1341
+ }
1342
+ compact = media.replace("-", "")
1343
+ return aliases.get(media, aliases.get(compact, media))
1344
+
1345
+
1346
+ def _pair_key(left: str, right: str) -> str:
1347
+ return " <-> ".join(sorted((left, right)))
1348
+
1349
+
1350
+ def select_capability_matrix_hits(
1351
+ plan: IntentPlan,
1352
+ samples: list[SampleDescriptor],
1353
+ ) -> list[CapabilityMatrixEntry]:
1354
+ matrix = build_capability_matrix(samples)
1355
+ requested_capabilities = set(plan.capabilities)
1356
+ if not requested_capabilities:
1357
+ return []
1358
+ requested_families = _requested_device_families(plan)
1359
+ hits: list[CapabilityMatrixEntry] = []
1360
+ seen: set[tuple[str, str]] = set()
1361
+ for entry in matrix:
1362
+ if entry.capability not in requested_capabilities:
1363
+ continue
1364
+ provider_families = _provider_families_for_capability(entry.capability, requested_families)
1365
+ if provider_families and entry.device_family not in provider_families:
1366
+ continue
1367
+ key = (entry.device_family, entry.capability)
1368
+ if key in seen:
1369
+ continue
1370
+ hits.append(entry)
1371
+ seen.add(key)
1372
+ return hits
1373
+
1374
+
1375
+ def build_blueprint_plan(plan: IntentPlan, blueprint: dict[str, object] | None = None) -> BlueprintPlan:
1376
+ devices = [dict(device) for device in (blueprint or {}).get("devices", [])]
1377
+ if not devices:
1378
+ devices = [
1379
+ {"type": device_type, "count": count}
1380
+ for device_type, count in sorted(plan.device_requirements.items())
1381
+ if count
1382
+ ]
1383
+ links = [dict(link) for link in (blueprint or {}).get("links", [])]
1384
+ if not links:
1385
+ links = [dict(link) for link in plan.links]
1386
+ required_pairs: list[str] = []
1387
+ for link in links:
1388
+ left = str(link.get("a", {}).get("dev", ""))
1389
+ right = str(link.get("b", {}).get("dev", ""))
1390
+ if left and right:
1391
+ required_pairs.append(" <-> ".join(sorted((left, right))))
1392
+ return BlueprintPlan(
1393
+ requested_devices=devices,
1394
+ requested_links=links,
1395
+ required_capabilities=sorted(dict.fromkeys(plan.capabilities)),
1396
+ required_donor_capacity={key: value for key, value in sorted(plan.device_requirements.items()) if value},
1397
+ required_link_pairs=sorted(dict.fromkeys(required_pairs)),
1398
+ )
1399
+
1400
+
1401
+ def build_coverage_gap_report(
1402
+ plan: IntentPlan,
1403
+ samples: list[SampleDescriptor],
1404
+ *,
1405
+ selected_donor: str | None = None,
1406
+ ) -> CoverageGapReport:
1407
+ iot_acceptance_gated = {"iot", "iot_registration", "iot_control"}
1408
+ selected_sample = _selected_sample_for_donor(samples, selected_donor)
1409
+ matrix = build_capability_matrix(samples)
1410
+ by_capability: dict[str, list[CapabilityMatrixEntry]] = {}
1411
+ by_family: dict[str, list[CapabilityMatrixEntry]] = {}
1412
+ for entry in matrix:
1413
+ by_capability.setdefault(entry.capability, []).append(entry)
1414
+ by_family.setdefault(entry.device_family, []).append(entry)
1415
+
1416
+ requested_families = _requested_device_families(plan)
1417
+ requested_capabilities = sorted(dict.fromkeys(plan.capabilities))
1418
+ supported_capabilities: list[str] = []
1419
+ unsupported_capabilities: list[str] = []
1420
+ requires_curated_donor: list[str] = []
1421
+ requires_manual_acceptance: list[str] = []
1422
+ capability_statuses: list[dict[str, object]] = []
1423
+ for capability in requested_capabilities:
1424
+ provider_families = _provider_families_for_capability(capability, requested_families)
1425
+ selected_donor_backed = _selected_donor_capability_override(plan, capability, selected_sample)
1426
+ wireless_edit_proven_only = (
1427
+ capability in WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES
1428
+ and _has_explicit_advanced_wireless_edit(plan, capability)
1429
+ and not selected_donor_backed
1430
+ )
1431
+ programming_edit_proven_only = (
1432
+ capability in PROGRAMMING_EDIT_PROVEN_CAPABILITIES
1433
+ and _has_explicit_programming_edit(plan, capability)
1434
+ and not selected_donor_backed
1435
+ )
1436
+ programming_donor_backed_ready = (
1437
+ capability in PROGRAMMING_DONOR_BACKED_READY_CAPABILITIES
1438
+ and programming_edit_proven_only
1439
+ )
1440
+ voice_edit_proven_only = (
1441
+ capability in VOICE_COLLABORATION_EDIT_PROVEN_CAPABILITIES
1442
+ and _has_explicit_voice_edit(plan, capability)
1443
+ and not selected_donor_backed
1444
+ )
1445
+ voice_donor_backed_ready = (
1446
+ capability in VOICE_COLLABORATION_DONOR_BACKED_READY_CAPABILITIES
1447
+ and voice_edit_proven_only
1448
+ )
1449
+ l2_security_edit_proven_only = (
1450
+ capability in {"dot1x", "qos"}
1451
+ and _has_explicit_l2_security_edit(plan, capability)
1452
+ and not selected_donor_backed
1453
+ )
1454
+ l2_security_donor_backed_ready = (
1455
+ capability in L2_SECURITY_MONITORING_DONOR_BACKED_READY_CAPABILITIES
1456
+ and l2_security_edit_proven_only
1457
+ )
1458
+ wan_security_edit_proven_only = (
1459
+ capability in WAN_SECURITY_EDIT_PROVEN_CAPABILITIES
1460
+ and _has_explicit_wan_security_edit(plan, capability)
1461
+ and not selected_donor_backed
1462
+ )
1463
+ l2_resiliency_edit_proven_only = (
1464
+ capability in L2_RESILIENCY_ROUTING_EDIT_PROVEN_CAPABILITIES
1465
+ and _has_explicit_l2_resiliency_edit(plan, capability)
1466
+ and not selected_donor_backed
1467
+ )
1468
+ ipv4_routing_management_edit_proven_only = (
1469
+ capability in IPV4_ROUTING_MANAGEMENT_EDIT_PROVEN_CAPABILITIES
1470
+ and _has_explicit_ipv4_routing_management_edit(plan, capability)
1471
+ and not selected_donor_backed
1472
+ )
1473
+ wan_security_donor_backed_ready = (
1474
+ capability in WAN_SECURITY_DONOR_BACKED_READY_EDIT_CAPABILITIES
1475
+ and wan_security_edit_proven_only
1476
+ )
1477
+ wan_security_donor_limited = (
1478
+ capability in WAN_SECURITY_DONOR_LIMITED_CAPABILITIES
1479
+ and _has_explicit_wan_security_intent(plan)
1480
+ and not selected_donor_backed
1481
+ and not wan_security_edit_proven_only
1482
+ )
1483
+ edit_proven_only = (
1484
+ capability in EDIT_PROVEN_REPORT_CAPABILITIES
1485
+ or wireless_edit_proven_only
1486
+ or programming_edit_proven_only
1487
+ or voice_edit_proven_only
1488
+ or l2_security_edit_proven_only
1489
+ or wan_security_edit_proven_only
1490
+ or l2_resiliency_edit_proven_only
1491
+ or ipv4_routing_management_edit_proven_only
1492
+ ) and not selected_donor_backed
1493
+ ipv6_donor_backed_ready = (
1494
+ capability in IPV6_DONOR_BACKED_READY_EDIT_CAPABILITIES
1495
+ and edit_proven_only
1496
+ )
1497
+ report_only = capability in REPORT_ONLY_CAPABILITIES and not selected_donor_backed and not edit_proven_only
1498
+ matching_entries = [
1499
+ entry
1500
+ for entry in by_capability.get(capability, [])
1501
+ if not provider_families or entry.device_family in provider_families
1502
+ ]
1503
+ if matching_entries:
1504
+ supported_capabilities.append(capability)
1505
+ best_entry = max(matching_entries, key=lambda entry: MATURITY_RANK[entry.maturity_level])
1506
+ acceptance_verified = (
1507
+ (any(entry.maturity_level == "acceptance-verified" for entry in matching_entries) and capability not in iot_acceptance_gated)
1508
+ or selected_donor_backed
1509
+ )
1510
+ if report_only or edit_proven_only or wan_security_donor_limited:
1511
+ acceptance_verified = False
1512
+ requires_curated = (
1513
+ best_entry.maturity_level == "safe-open-generate-supported"
1514
+ and not selected_donor_backed
1515
+ and not edit_proven_only
1516
+ ) or wan_security_donor_limited
1517
+ if requires_curated:
1518
+ requires_curated_donor.append(capability)
1519
+ if not acceptance_verified:
1520
+ requires_manual_acceptance.append(capability)
1521
+ capability_statuses.append(
1522
+ {
1523
+ "capability": capability,
1524
+ "provider_families": sorted(provider_families),
1525
+ "matching_device_families": sorted({entry.device_family for entry in matching_entries}),
1526
+ "best_maturity_level": "inventory-supported" if report_only else "config-mutation-supported" if edit_proven_only else best_entry.maturity_level,
1527
+ "inventory_supported": any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["inventory-supported"] for entry in matching_entries),
1528
+ "edit_supported": True if edit_proven_only else False if report_only else any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["config-mutation-supported"] for entry in matching_entries),
1529
+ "config_mutation_supported": True if edit_proven_only else False if report_only else any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["config-mutation-supported"] for entry in matching_entries),
1530
+ "safe_open_generate_supported": False if report_only or edit_proven_only or wan_security_donor_limited else any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["safe-open-generate-supported"] for entry in matching_entries),
1531
+ "acceptance_verified": acceptance_verified,
1532
+ "donor_backed_ready": bool(
1533
+ selected_donor_backed
1534
+ or ipv6_donor_backed_ready
1535
+ or programming_donor_backed_ready
1536
+ or voice_donor_backed_ready
1537
+ or l2_security_donor_backed_ready
1538
+ or wan_security_donor_backed_ready
1539
+ ),
1540
+ "requires_curated_donor": requires_curated,
1541
+ "requires_manual_acceptance": not acceptance_verified,
1542
+ "report_only": report_only,
1543
+ "donor_limited_generate": wan_security_donor_limited,
1544
+ "recommended_donors": list(dict.fromkeys(donor for entry in matching_entries for donor in entry.accepted_donors))[:10],
1545
+ "known_limitations": list(dict.fromkeys(item for entry in matching_entries for item in entry.known_limitations)),
1546
+ }
1547
+ )
1548
+ elif edit_proven_only:
1549
+ supported_capabilities.append(capability)
1550
+ requires_manual_acceptance.append(capability)
1551
+ capability_statuses.append(
1552
+ {
1553
+ "capability": capability,
1554
+ "provider_families": sorted(provider_families),
1555
+ "matching_device_families": sorted(provider_families),
1556
+ "best_maturity_level": "config-mutation-supported",
1557
+ "inventory_supported": True,
1558
+ "edit_supported": True,
1559
+ "config_mutation_supported": True,
1560
+ "safe_open_generate_supported": False,
1561
+ "acceptance_verified": False,
1562
+ "donor_backed_ready": bool(
1563
+ programming_donor_backed_ready
1564
+ or ipv6_donor_backed_ready
1565
+ or voice_donor_backed_ready
1566
+ or l2_security_donor_backed_ready
1567
+ or wan_security_donor_backed_ready
1568
+ ),
1569
+ "requires_curated_donor": False,
1570
+ "requires_manual_acceptance": True,
1571
+ "report_only": False,
1572
+ "donor_limited_generate": False,
1573
+ "recommended_donors": [],
1574
+ "known_limitations": ["editor-proven without sample-backed donor selection"],
1575
+ }
1576
+ )
1577
+ elif report_only:
1578
+ supported_capabilities.append(capability)
1579
+ requires_manual_acceptance.append(capability)
1580
+ capability_statuses.append(
1581
+ {
1582
+ "capability": capability,
1583
+ "provider_families": sorted(provider_families),
1584
+ "matching_device_families": sorted(provider_families),
1585
+ "best_maturity_level": "inventory-supported",
1586
+ "inventory_supported": True,
1587
+ "edit_supported": False,
1588
+ "config_mutation_supported": False,
1589
+ "safe_open_generate_supported": False,
1590
+ "acceptance_verified": False,
1591
+ "donor_backed_ready": False,
1592
+ "requires_curated_donor": False,
1593
+ "requires_manual_acceptance": True,
1594
+ "report_only": True,
1595
+ "donor_limited_generate": False,
1596
+ "recommended_donors": [],
1597
+ "known_limitations": ["report-supported without decode-backed donor selection"],
1598
+ }
1599
+ )
1600
+ elif selected_donor_backed and selected_sample is not None:
1601
+ supported_capabilities.append(capability)
1602
+ capability_statuses.append(
1603
+ {
1604
+ "capability": capability,
1605
+ "provider_families": sorted(provider_families),
1606
+ "matching_device_families": sorted({family for family in (selected_sample.device_families or []) if not provider_families or family in provider_families}),
1607
+ "best_maturity_level": selected_sample.apply_safety_level or "safe-open-generate-supported",
1608
+ "inventory_supported": True,
1609
+ "edit_supported": True,
1610
+ "config_mutation_supported": True,
1611
+ "safe_open_generate_supported": True,
1612
+ "acceptance_verified": True,
1613
+ "donor_backed_ready": True,
1614
+ "requires_curated_donor": False,
1615
+ "requires_manual_acceptance": False,
1616
+ "donor_limited_generate": False,
1617
+ "recommended_donors": [selected_sample.relative_path],
1618
+ "known_limitations": [],
1619
+ }
1620
+ )
1621
+ else:
1622
+ unsupported_capabilities.append(capability)
1623
+ capability_statuses.append(
1624
+ {
1625
+ "capability": capability,
1626
+ "provider_families": sorted(provider_families),
1627
+ "matching_device_families": [],
1628
+ "best_maturity_level": "reference-known",
1629
+ "inventory_supported": False,
1630
+ "edit_supported": False,
1631
+ "config_mutation_supported": False,
1632
+ "safe_open_generate_supported": False,
1633
+ "acceptance_verified": False,
1634
+ "donor_backed_ready": False,
1635
+ "requires_curated_donor": False,
1636
+ "requires_manual_acceptance": True,
1637
+ "donor_limited_generate": False,
1638
+ "recommended_donors": [],
1639
+ "known_limitations": ["no matching donor coverage found"],
1640
+ }
1641
+ )
1642
+
1643
+ unsupported_families = [family for family in requested_families if family not in by_family]
1644
+
1645
+ requires_curated_donor = sorted(dict.fromkeys(requires_curated_donor))
1646
+ requires_manual_acceptance = sorted(dict.fromkeys(requires_manual_acceptance))
1647
+ capability_parity = [
1648
+ {
1649
+ "capability": str(status.get("capability")),
1650
+ "inventory_supported": bool(status.get("inventory_supported")),
1651
+ "edit_supported": bool(status.get("edit_supported")),
1652
+ "donor_backed_ready": _donor_backed_ready(status),
1653
+ "generate_supported": bool(status.get("safe_open_generate_supported")),
1654
+ "acceptance_verified": bool(status.get("acceptance_verified")),
1655
+ "best_maturity_level": _best_maturity_level(status),
1656
+ "generate_mismatch_reason": _generate_mismatch_reason(status),
1657
+ "required_donor_families": sorted(_provider_families_for_capability(str(status.get("capability")), requested_families)),
1658
+ "required_archetypes": CAPABILITY_REQUIRED_ARCHETYPES.get(str(status.get("capability")), []),
1659
+ "required_runtime_features": CAPABILITY_REQUIRED_RUNTIME_FEATURES.get(str(status.get("capability")), []),
1660
+ "recommended_next_action": _recommended_next_action_for_capability(
1661
+ str(status.get("capability")),
1662
+ _generate_mismatch_reason(status),
1663
+ ),
1664
+ }
1665
+ for status in capability_statuses
1666
+ ]
1667
+ scenario_family = _scenario_family_for_plan(plan, requested_families)
1668
+ if selected_donor is None and plan.blocked_mutations:
1669
+ requires_manual_acceptance = sorted(dict.fromkeys([*requires_manual_acceptance, *plan.blocked_mutations]))
1670
+ scenario_generate_readiness = _scenario_generate_readiness(
1671
+ scenario_family,
1672
+ capability_statuses,
1673
+ unsupported_capabilities,
1674
+ requires_curated_donor,
1675
+ requires_manual_acceptance,
1676
+ )
1677
+ recommended_next_actions: list[str] = []
1678
+ if unsupported_capabilities or unsupported_families:
1679
+ recommended_next_actions.append(
1680
+ "Inspect the blueprint and import or provide a donor that contains the missing capabilities or device families."
1681
+ )
1682
+ if requires_manual_acceptance:
1683
+ recommended_next_actions.append(
1684
+ "Run explain-plan or validate-open-debug before enabling the acceptance-gated capability set."
1685
+ )
1686
+ if not requested_capabilities and not requested_families:
1687
+ recommended_next_actions.append("Add explicit device, service, or topology requirements so donor ranking can be constrained.")
1688
+
1689
+ return CoverageGapReport(
1690
+ requested_capabilities=requested_capabilities,
1691
+ supported_capabilities=supported_capabilities,
1692
+ unsupported_capabilities=unsupported_capabilities,
1693
+ requested_device_families=requested_families,
1694
+ unsupported_device_families=unsupported_families,
1695
+ requires_curated_donor=requires_curated_donor,
1696
+ requires_manual_acceptance=requires_manual_acceptance,
1697
+ capability_statuses=capability_statuses,
1698
+ capability_parity=capability_parity,
1699
+ scenario_family=scenario_family,
1700
+ scenario_generate_readiness=scenario_generate_readiness,
1701
+ recommended_next_actions=recommended_next_actions,
1702
+ )
1703
+
1704
+
1705
+ def build_inventory_capability_report(payload: dict[str, Any]) -> dict[str, object]:
1706
+ device_counts = dict(payload.get("topology_summary", {}).get("device_counts", {}))
1707
+ families = sorted({_device_family_for_type(device_type) for device_type in device_counts})
1708
+ capabilities: set[str] = set()
1709
+ if "multilayer switches" in families:
1710
+ capabilities.add("multilayer_switching")
1711
+ if "wan/cloud/dsl/cable devices" in families:
1712
+ capabilities.add("wan")
1713
+ if "security devices" in families:
1714
+ capabilities.add("security_edge")
1715
+ if payload.get("vlans"):
1716
+ capabilities.update(["vlan", "trunk", "access_port"])
1717
+ if payload.get("dhcp_pools"):
1718
+ capabilities.update(["dhcp_pool", "router_dhcp", "server_dhcp"])
1719
+ if payload.get("services"):
1720
+ capabilities.update(
1721
+ {
1722
+ "dns" if "dns_server" in service else service
1723
+ for service_names in payload["services"].values()
1724
+ for service in service_names
1725
+ }
1726
+ )
1727
+ if payload.get("wireless"):
1728
+ capabilities.update(["wireless_ap", "wireless_mutation", "wireless_client"])
1729
+ if any(family == "end devices" for family in families):
1730
+ capabilities.add("wireless_client_association")
1731
+ if payload.get("iot"):
1732
+ capabilities.add("iot")
1733
+ iot_roles = {str(entry.get("role", "")).strip() for entry in payload["iot"].values() if str(entry.get("role", "")).strip()}
1734
+ if "thing" in iot_roles and ("gateway" in iot_roles or "server" in iot_roles):
1735
+ capabilities.add("iot_registration")
1736
+ if "thing" in iot_roles and ("gateway" in iot_roles or "server" in iot_roles):
1737
+ capabilities.add("iot_control")
1738
+ if any(family == "end devices" for family in families):
1739
+ capabilities.add("end_device_mutation")
1740
+ if payload.get("acl_names"):
1741
+ capabilities.add("acl")
1742
+ if payload.get("topology_summary", {}).get("network_style") == "wan_security":
1743
+ capabilities.update({"vpn", "ipsec", "gre", "ppp"})
1744
+ for routing_details in payload.get("routing", {}).values():
1745
+ capabilities.update(str(capability) for capability in routing_details.get("capabilities", []) if str(capability).strip())
1746
+ for l2_details in payload.get("l2_security_monitoring", {}).values():
1747
+ capabilities.update(str(capability) for capability in l2_details.get("capabilities", []) if str(capability).strip())
1748
+ for l2_resiliency_details in payload.get("l2_resiliency_routing", {}).values():
1749
+ capabilities.update(str(capability) for capability in l2_resiliency_details.get("capabilities", []) if str(capability).strip())
1750
+ for ipv4_details in payload.get("ipv4_routing_management", {}).values():
1751
+ capabilities.update(str(capability) for capability in ipv4_details.get("capabilities", []) if str(capability).strip())
1752
+ for voice_details in payload.get("voice", {}).values():
1753
+ capabilities.update(str(capability) for capability in voice_details.get("capabilities", []) if str(capability).strip())
1754
+ for programming_details in payload.get("programming", {}).values():
1755
+ capabilities.update(str(capability) for capability in programming_details.get("feature_tags", []) if str(capability).strip())
1756
+ return {
1757
+ "device_families": families,
1758
+ "capabilities": sorted(capabilities),
1759
+ "device_counts": device_counts,
1760
+ }
1761
+
1762
+
1763
+ def build_donor_graph_fit(sample: SampleDescriptor, blueprint: dict[str, object] | None = None) -> DonorGraphFit:
1764
+ expected_pairs: list[str] = []
1765
+ expected_media: dict[str, str] = {}
1766
+ expected_ports: dict[str, set[str]] = {}
1767
+ blueprint_archetype = ""
1768
+ blueprint_device_count = 0
1769
+ if blueprint:
1770
+ blueprint_archetype = str(blueprint.get("topology_archetype", "")).strip()
1771
+ blueprint_device_count = len(list(blueprint.get("devices", [])))
1772
+ for link in blueprint.get("links", []):
1773
+ left = str(link.get("a", {}).get("dev", ""))
1774
+ right = str(link.get("b", {}).get("dev", ""))
1775
+ if left and right:
1776
+ pair = _pair_key(left, right)
1777
+ expected_pairs.append(pair)
1778
+ media = _normalize_media_name(str(link.get("media", "")))
1779
+ if media:
1780
+ expected_media[pair] = media
1781
+ ports = {
1782
+ str(link.get("a", {}).get("port", "")).strip(),
1783
+ str(link.get("b", {}).get("port", "")).strip(),
1784
+ }
1785
+ expected_ports[pair] = {port for port in ports if port}
1786
+ sample_pairs: set[str] = set()
1787
+ sample_media: dict[str, str] = {}
1788
+ sample_ports: dict[str, set[str]] = {}
1789
+ for link in sample.links:
1790
+ left = str(link.get("from", "")).strip()
1791
+ right = str(link.get("to", "")).strip()
1792
+ if left and right:
1793
+ pair = _pair_key(left, right)
1794
+ sample_pairs.add(pair)
1795
+ media = _normalize_media_name(str(link.get("media", "") or link.get("cable_type", "")))
1796
+ if media and pair not in sample_media:
1797
+ sample_media[pair] = media
1798
+ ports = {str(port).strip() for port in link.get("ports", []) if str(port).strip()}
1799
+ if ports:
1800
+ sample_ports.setdefault(pair, set()).update(ports)
1801
+ matched_pairs = [pair for pair in expected_pairs if pair in sample_pairs]
1802
+ missing_pairs = [pair for pair in expected_pairs if pair not in sample_pairs]
1803
+ port_media_conflicts: list[str] = []
1804
+ for pair in matched_pairs:
1805
+ expected_pair_media = expected_media.get(pair)
1806
+ sample_pair_media = sample_media.get(pair)
1807
+ if expected_pair_media and sample_pair_media and expected_pair_media != sample_pair_media:
1808
+ port_media_conflicts.append(f"{pair} media mismatch ({expected_pair_media} != {sample_pair_media})")
1809
+ expected_pair_ports = expected_ports.get(pair, set())
1810
+ sample_pair_ports = sample_ports.get(pair, set())
1811
+ if expected_pair_ports and sample_pair_ports and not expected_pair_ports.issubset(sample_pair_ports):
1812
+ port_media_conflicts.append(
1813
+ f"{pair} port mismatch (expected {sorted(expected_pair_ports)}, available {sorted(sample_pair_ports)})"
1814
+ )
1815
+ fit_score = max(0, len(matched_pairs) * 10 - len(missing_pairs) * 5 - len(port_media_conflicts) * 3)
1816
+ topology_bonus = 0
1817
+ if blueprint_archetype:
1818
+ sample_topology = set(sample.topology_tags)
1819
+ if blueprint_archetype in sample_topology:
1820
+ topology_bonus += 18
1821
+ elif blueprint_archetype == "chain" and sample_topology & {"department_lan", "core_access"}:
1822
+ topology_bonus += 12
1823
+ elif blueprint_archetype == "core_access" and sample_topology & {"department_lan", "router_on_a_stick"}:
1824
+ topology_bonus += 10
1825
+ elif blueprint_archetype == "wireless_branch" and sample_topology & {"wireless_edge", "small_office"}:
1826
+ topology_bonus += 10
1827
+ capacity_penalty = max(0, blueprint_device_count - sample.device_count) * 2
1828
+ layout_reuse_score = max(
1829
+ 0,
1830
+ len(matched_pairs) * 12 + topology_bonus - len(missing_pairs) * 6 - len(port_media_conflicts) * 4 - capacity_penalty,
1831
+ )
1832
+ return DonorGraphFit(
1833
+ matched_pairs=matched_pairs,
1834
+ missing_pairs=missing_pairs,
1835
+ port_media_conflicts=port_media_conflicts,
1836
+ fit_score=fit_score,
1837
+ layout_reuse_score=layout_reuse_score,
1838
+ )
1839
+
1840
+
1841
+ def asdict_list(items: list[Any]) -> list[dict[str, object]]:
1842
+ return [asdict(item) for item in items]