packet-tracer-skill 0.2.1 → 0.2.3

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 (43) hide show
  1. package/CHANGELOG.md +53 -2
  2. package/README.md +443 -124
  3. package/docs/automation-controller-proof.md +35 -0
  4. package/docs/campus-donor-proof.md +103 -0
  5. package/docs/curated-donor-registry.md +37 -0
  6. package/docs/generate-ready-pilot-design.md +30 -0
  7. package/docs/github-launch-ops-0.2.1.md +45 -0
  8. package/docs/github-launch-ops-0.2.2.md +42 -0
  9. package/docs/github-metadata.md +23 -6
  10. package/docs/home-iot-donor-proof.md +64 -0
  11. package/docs/industrial-programming-proof.md +48 -0
  12. package/docs/ipv4-routing-management-proof.md +37 -0
  13. package/docs/l2-resiliency-bgp-proof.md +60 -0
  14. package/docs/l2-security-qos-proof.md +59 -0
  15. package/docs/launch-announcement-0.2.2.md +15 -0
  16. package/docs/packet-tracer-feature-gap-atlas.md +244 -0
  17. package/docs/post-launch-follow-up.md +39 -0
  18. package/docs/publish-preview-roadmap.md +20 -17
  19. package/docs/release-checklist.md +32 -17
  20. package/docs/release-notes-0.2.1.md +7 -6
  21. package/docs/release-notes-0.2.2.md +26 -0
  22. package/docs/release-notes-0.2.3.md +59 -0
  23. package/docs/runtime-truth.md +31 -0
  24. package/docs/security-edge-deepening-proof.md +65 -0
  25. package/docs/voice-collaboration-proof.md +38 -0
  26. package/docs/wan-security-donor-proof.md +58 -0
  27. package/docs/wireless-advanced-proof.md +60 -0
  28. package/examples/README.md +11 -8
  29. package/examples/gallery.md +9 -3
  30. package/examples/index.json +3 -3
  31. package/package.json +20 -1
  32. package/references/curated-donor-registry.json +5 -2
  33. package/references/packettracer-feature-atlas.json +183 -0
  34. package/references/scenario-fixture-corpus.json +1 -0
  35. package/scripts/build_examples_index.py +26 -15
  36. package/scripts/coverage_matrix.py +1054 -25
  37. package/scripts/feature_atlas.py +293 -0
  38. package/scripts/generate_pkt.py +491 -34
  39. package/scripts/intent_parser.py +798 -8
  40. package/scripts/pkt_editor.py +651 -0
  41. package/scripts/remote_search.py +197 -21
  42. package/scripts/runtime_doctor.py +83 -10
  43. package/scripts/sample_catalog.py +266 -6
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
3
4
  from dataclasses import asdict, dataclass, field
4
5
  from typing import Any
5
6
 
@@ -26,6 +27,9 @@ DEVICE_FAMILY_MAP = {
26
27
  "Tablet": "end devices",
27
28
  "Smartphone": "end devices",
28
29
  "Printer": "end devices",
30
+ "IpPhone": "end devices",
31
+ "HomeVoip": "end devices",
32
+ "AnalogPhone": "end devices",
29
33
  "LightWeightAccessPoint": "access points",
30
34
  "WirelessRouter": "home/wireless routers",
31
35
  "WirelessRouterNewGeneration": "home/wireless routers",
@@ -37,6 +41,9 @@ DEVICE_FAMILY_MAP = {
37
41
  "Dsl Modem": "wan/cloud/dsl/cable devices",
38
42
  "Security Appliance": "security devices",
39
43
  "ASA": "security devices",
44
+ "NetworkController": "network controllers",
45
+ "CentralOfficeServer": "wan/cloud/dsl/cable devices",
46
+ "CellTower": "wan/cloud/dsl/cable devices",
40
47
  "IoT": "iot devices",
41
48
  "Board": "iot devices",
42
49
  "Sensor": "iot devices",
@@ -52,6 +59,201 @@ DEFAULT_FAMILY_LIMITATIONS = {
52
59
  "security devices": ["advanced security devices remain donor-limited"],
53
60
  }
54
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
+
55
257
  CAPABILITY_PROVIDER_FAMILIES = {
56
258
  "access_port": {"switches", "multilayer switches"},
57
259
  "trunk": {"switches", "multilayer switches"},
@@ -70,6 +272,25 @@ CAPABILITY_PROVIDER_FAMILIES = {
70
272
  "ipsec": {"routers", "security devices", "wan/cloud/dsl/cable devices"},
71
273
  "gre": {"routers", "wan/cloud/dsl/cable devices"},
72
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"},
73
294
  "multilayer_switching": {"multilayer switches"},
74
295
  "wan": {"wan/cloud/dsl/cable devices", "routers"},
75
296
  "security_edge": {"security devices", "routers"},
@@ -99,6 +320,67 @@ CAPABILITY_PROVIDER_FAMILIES = {
99
320
  "tablet": {"end devices"},
100
321
  "printer": {"end devices"},
101
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"},
102
384
  }
103
385
 
104
386
 
@@ -126,6 +408,33 @@ class CoverageGapReport:
126
408
  scenario_generate_readiness: dict[str, object] = field(default_factory=dict)
127
409
  recommended_next_actions: list[str] = field(default_factory=list)
128
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
+
129
438
 
130
439
  @dataclass
131
440
  class BlueprintPlan:
@@ -277,26 +586,78 @@ def _requested_device_families(plan: IntentPlan) -> list[str]:
277
586
  def _scenario_family_for_plan(plan: IntentPlan, requested_families: list[str]) -> str | None:
278
587
  capabilities = set(plan.capabilities)
279
588
  requested_services = {str(service) for service in plan.service_requirements.get("services", []) if service}
280
- if capabilities & {"iot", "iot_registration", "iot_control"} or {"iot devices", "home/wireless routers"} & set(requested_families):
281
- return "home_iot"
282
- if (
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 = (
283
616
  plan.network_style == "wan_security"
284
- or capabilities & {"vpn", "ipsec", "gre", "ppp", "multilayer_switching", "security_edge"}
285
- or {"wan/cloud/dsl/cable devices", "security devices"} & set(requested_families)
286
- ):
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:
287
646
  return "wan_security_edge"
288
- if (
289
- requested_services & {"dns", "dhcp", "http", "https", "ftp", "tftp", "email", "syslog", "aaa", "ntp"}
290
- or capabilities & {"server_dns", "server_dhcp", "server_http", "server_https", "server_ftp", "server_tftp", "server_email", "server_syslog", "server_aaa"}
291
- or "servers" in requested_families
292
- ) and not (plan.department_groups or {"switches", "multilayer switches"} & set(requested_families) and capabilities & {"vlan", "router_on_a_stick", "management_vlan", "telnet"}):
293
- return "service_heavy"
294
- if (
295
- plan.department_groups
296
- or {"switches", "multilayer switches"} & set(requested_families)
297
- or capabilities & {"vlan", "router_on_a_stick", "management_vlan", "telnet", "acl", "nat", "pat"}
298
- ):
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:
299
658
  return "campus"
659
+ if service_signal:
660
+ return "service_heavy"
300
661
  return None
301
662
 
302
663
 
@@ -304,7 +665,16 @@ SCENARIO_CAPABILITY_SETS = {
304
665
  "campus": {"router_on_a_stick", "trunk", "access_port", "management_vlan", "telnet", "verification", "acl", "nat"},
305
666
  "service_heavy": {"server_dns", "server_dhcp", "server_http", "server_https", "server_ftp", "server_tftp", "server_email", "server_syslog", "server_aaa", "ntp"},
306
667
  "home_iot": {"iot", "iot_registration", "iot_control", "wireless_ap", "wireless_mutation"},
307
- "wan_security_edge": {"vpn", "ipsec", "gre", "ppp", "acl", "nat"},
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"},
308
678
  }
309
679
 
310
680
  CAPABILITY_REQUIRED_ARCHETYPES = {
@@ -330,6 +700,76 @@ CAPABILITY_REQUIRED_ARCHETYPES = {
330
700
  "ppp": ["WAN/security edge"],
331
701
  "multilayer_switching": ["WAN/security edge", "campus/core"],
332
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"],
333
773
  }
334
774
 
335
775
  CAPABILITY_REQUIRED_RUNTIME_FEATURES = {
@@ -346,12 +786,44 @@ CAPABILITY_REQUIRED_RUNTIME_FEATURES = {
346
786
  "wireless_client_association": ["workspace_validated", "wireless_runtime"],
347
787
  "iot_registration": ["workspace_validated", "iot_runtime"],
348
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"],
349
817
  }
350
818
 
351
819
 
352
820
  def _generate_mismatch_reason(status: dict[str, object]) -> str | None:
821
+ if bool(status.get("report_only")):
822
+ return "report_only"
353
823
  if not bool(status.get("inventory_supported")):
354
824
  return "unsupported"
825
+ if bool(status.get("donor_limited_generate")):
826
+ return "supported_but_donor_limited"
355
827
  if bool(status.get("edit_supported")) and not bool(status.get("safe_open_generate_supported")):
356
828
  return "supported_in_edit_only"
357
829
  if bool(status.get("safe_open_generate_supported")) and bool(status.get("requires_curated_donor")):
@@ -363,6 +835,327 @@ def _generate_mismatch_reason(status: dict[str, object]) -> str | None:
363
835
  return "unsupported"
364
836
 
365
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
+
366
1159
  def _best_maturity_level(status: dict[str, object]) -> str:
367
1160
  level = str(status.get("best_maturity_level") or "").strip()
368
1161
  if level in MATURITY_RANK:
@@ -381,6 +1174,60 @@ def _best_maturity_level(status: dict[str, object]) -> str:
381
1174
 
382
1175
 
383
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."
384
1231
  if mismatch_reason == "supported_in_edit_only":
385
1232
  return f"Use donor-backed edit flow for {capability} until safe-open generate coverage is raised."
386
1233
  if mismatch_reason == "supported_but_donor_limited":
@@ -528,6 +1375,7 @@ def build_coverage_gap_report(
528
1375
  selected_donor: str | None = None,
529
1376
  ) -> CoverageGapReport:
530
1377
  iot_acceptance_gated = {"iot", "iot_registration", "iot_control"}
1378
+ selected_sample = _selected_sample_for_donor(samples, selected_donor)
531
1379
  matrix = build_capability_matrix(samples)
532
1380
  by_capability: dict[str, list[CapabilityMatrixEntry]] = {}
533
1381
  by_family: dict[str, list[CapabilityMatrixEntry]] = {}
@@ -544,6 +1392,79 @@ def build_coverage_gap_report(
544
1392
  capability_statuses: list[dict[str, object]] = []
545
1393
  for capability in requested_capabilities:
546
1394
  provider_families = _provider_families_for_capability(capability, requested_families)
1395
+ selected_donor_backed = _selected_donor_capability_override(plan, capability, selected_sample)
1396
+ wireless_edit_proven_only = (
1397
+ capability in WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES
1398
+ and _has_explicit_advanced_wireless_edit(plan, capability)
1399
+ and not selected_donor_backed
1400
+ )
1401
+ programming_edit_proven_only = (
1402
+ capability in PROGRAMMING_EDIT_PROVEN_CAPABILITIES
1403
+ and _has_explicit_programming_edit(plan, capability)
1404
+ and not selected_donor_backed
1405
+ )
1406
+ programming_donor_backed_ready = (
1407
+ capability in PROGRAMMING_DONOR_BACKED_READY_CAPABILITIES
1408
+ and programming_edit_proven_only
1409
+ )
1410
+ voice_edit_proven_only = (
1411
+ capability in VOICE_COLLABORATION_EDIT_PROVEN_CAPABILITIES
1412
+ and _has_explicit_voice_edit(plan, capability)
1413
+ and not selected_donor_backed
1414
+ )
1415
+ voice_donor_backed_ready = (
1416
+ capability in VOICE_COLLABORATION_DONOR_BACKED_READY_CAPABILITIES
1417
+ and voice_edit_proven_only
1418
+ )
1419
+ l2_security_edit_proven_only = (
1420
+ capability in {"dot1x", "qos"}
1421
+ and _has_explicit_l2_security_edit(plan, capability)
1422
+ and not selected_donor_backed
1423
+ )
1424
+ l2_security_donor_backed_ready = (
1425
+ capability in L2_SECURITY_MONITORING_DONOR_BACKED_READY_CAPABILITIES
1426
+ and l2_security_edit_proven_only
1427
+ )
1428
+ wan_security_edit_proven_only = (
1429
+ capability in WAN_SECURITY_EDIT_PROVEN_CAPABILITIES
1430
+ and _has_explicit_wan_security_edit(plan, capability)
1431
+ and not selected_donor_backed
1432
+ )
1433
+ l2_resiliency_edit_proven_only = (
1434
+ capability in L2_RESILIENCY_ROUTING_EDIT_PROVEN_CAPABILITIES
1435
+ and _has_explicit_l2_resiliency_edit(plan, capability)
1436
+ and not selected_donor_backed
1437
+ )
1438
+ ipv4_routing_management_edit_proven_only = (
1439
+ capability in IPV4_ROUTING_MANAGEMENT_EDIT_PROVEN_CAPABILITIES
1440
+ and _has_explicit_ipv4_routing_management_edit(plan, capability)
1441
+ and not selected_donor_backed
1442
+ )
1443
+ wan_security_donor_backed_ready = (
1444
+ capability in WAN_SECURITY_DONOR_BACKED_READY_EDIT_CAPABILITIES
1445
+ and wan_security_edit_proven_only
1446
+ )
1447
+ wan_security_donor_limited = (
1448
+ capability in WAN_SECURITY_DONOR_LIMITED_CAPABILITIES
1449
+ and _has_explicit_wan_security_intent(plan)
1450
+ and not selected_donor_backed
1451
+ and not wan_security_edit_proven_only
1452
+ )
1453
+ edit_proven_only = (
1454
+ capability in EDIT_PROVEN_REPORT_CAPABILITIES
1455
+ or wireless_edit_proven_only
1456
+ or programming_edit_proven_only
1457
+ or voice_edit_proven_only
1458
+ or l2_security_edit_proven_only
1459
+ or wan_security_edit_proven_only
1460
+ or l2_resiliency_edit_proven_only
1461
+ or ipv4_routing_management_edit_proven_only
1462
+ ) and not selected_donor_backed
1463
+ ipv6_donor_backed_ready = (
1464
+ capability in IPV6_DONOR_BACKED_READY_EDIT_CAPABILITIES
1465
+ and edit_proven_only
1466
+ )
1467
+ report_only = capability in REPORT_ONLY_CAPABILITIES and not selected_donor_backed and not edit_proven_only
547
1468
  matching_entries = [
548
1469
  entry
549
1470
  for entry in by_capability.get(capability, [])
@@ -552,8 +1473,18 @@ def build_coverage_gap_report(
552
1473
  if matching_entries:
553
1474
  supported_capabilities.append(capability)
554
1475
  best_entry = max(matching_entries, key=lambda entry: MATURITY_RANK[entry.maturity_level])
555
- acceptance_verified = any(entry.maturity_level == "acceptance-verified" for entry in matching_entries) and capability not in iot_acceptance_gated
556
- if best_entry.maturity_level == "safe-open-generate-supported":
1476
+ acceptance_verified = (
1477
+ (any(entry.maturity_level == "acceptance-verified" for entry in matching_entries) and capability not in iot_acceptance_gated)
1478
+ or selected_donor_backed
1479
+ )
1480
+ if report_only or edit_proven_only or wan_security_donor_limited:
1481
+ acceptance_verified = False
1482
+ requires_curated = (
1483
+ best_entry.maturity_level == "safe-open-generate-supported"
1484
+ and not selected_donor_backed
1485
+ and not edit_proven_only
1486
+ ) or wan_security_donor_limited
1487
+ if requires_curated:
557
1488
  requires_curated_donor.append(capability)
558
1489
  if not acceptance_verified:
559
1490
  requires_manual_acceptance.append(capability)
@@ -562,18 +1493,101 @@ def build_coverage_gap_report(
562
1493
  "capability": capability,
563
1494
  "provider_families": sorted(provider_families),
564
1495
  "matching_device_families": sorted({entry.device_family for entry in matching_entries}),
565
- "best_maturity_level": best_entry.maturity_level,
1496
+ "best_maturity_level": "inventory-supported" if report_only else "config-mutation-supported" if edit_proven_only else best_entry.maturity_level,
566
1497
  "inventory_supported": any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["inventory-supported"] for entry in matching_entries),
567
- "edit_supported": any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["config-mutation-supported"] for entry in matching_entries),
568
- "config_mutation_supported": any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["config-mutation-supported"] for entry in matching_entries),
569
- "safe_open_generate_supported": any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["safe-open-generate-supported"] for entry in matching_entries),
1498
+ "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),
1499
+ "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),
1500
+ "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),
570
1501
  "acceptance_verified": acceptance_verified,
571
- "requires_curated_donor": best_entry.maturity_level == "safe-open-generate-supported",
1502
+ "donor_backed_ready": bool(
1503
+ selected_donor_backed
1504
+ or ipv6_donor_backed_ready
1505
+ or programming_donor_backed_ready
1506
+ or voice_donor_backed_ready
1507
+ or l2_security_donor_backed_ready
1508
+ or wan_security_donor_backed_ready
1509
+ ),
1510
+ "requires_curated_donor": requires_curated,
572
1511
  "requires_manual_acceptance": not acceptance_verified,
1512
+ "report_only": report_only,
1513
+ "donor_limited_generate": wan_security_donor_limited,
573
1514
  "recommended_donors": list(dict.fromkeys(donor for entry in matching_entries for donor in entry.accepted_donors))[:10],
574
1515
  "known_limitations": list(dict.fromkeys(item for entry in matching_entries for item in entry.known_limitations)),
575
1516
  }
576
1517
  )
1518
+ elif edit_proven_only:
1519
+ supported_capabilities.append(capability)
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(provider_families),
1526
+ "best_maturity_level": "config-mutation-supported",
1527
+ "inventory_supported": True,
1528
+ "edit_supported": True,
1529
+ "config_mutation_supported": True,
1530
+ "safe_open_generate_supported": False,
1531
+ "acceptance_verified": False,
1532
+ "donor_backed_ready": bool(
1533
+ programming_donor_backed_ready
1534
+ or ipv6_donor_backed_ready
1535
+ or voice_donor_backed_ready
1536
+ or l2_security_donor_backed_ready
1537
+ or wan_security_donor_backed_ready
1538
+ ),
1539
+ "requires_curated_donor": False,
1540
+ "requires_manual_acceptance": True,
1541
+ "report_only": False,
1542
+ "donor_limited_generate": False,
1543
+ "recommended_donors": [],
1544
+ "known_limitations": ["editor-proven without sample-backed donor selection"],
1545
+ }
1546
+ )
1547
+ elif report_only:
1548
+ supported_capabilities.append(capability)
1549
+ requires_manual_acceptance.append(capability)
1550
+ capability_statuses.append(
1551
+ {
1552
+ "capability": capability,
1553
+ "provider_families": sorted(provider_families),
1554
+ "matching_device_families": sorted(provider_families),
1555
+ "best_maturity_level": "inventory-supported",
1556
+ "inventory_supported": True,
1557
+ "edit_supported": False,
1558
+ "config_mutation_supported": False,
1559
+ "safe_open_generate_supported": False,
1560
+ "acceptance_verified": False,
1561
+ "donor_backed_ready": False,
1562
+ "requires_curated_donor": False,
1563
+ "requires_manual_acceptance": True,
1564
+ "report_only": True,
1565
+ "donor_limited_generate": False,
1566
+ "recommended_donors": [],
1567
+ "known_limitations": ["report-supported without decode-backed donor selection"],
1568
+ }
1569
+ )
1570
+ elif selected_donor_backed and selected_sample is not None:
1571
+ supported_capabilities.append(capability)
1572
+ capability_statuses.append(
1573
+ {
1574
+ "capability": capability,
1575
+ "provider_families": sorted(provider_families),
1576
+ "matching_device_families": sorted({family for family in (selected_sample.device_families or []) if not provider_families or family in provider_families}),
1577
+ "best_maturity_level": selected_sample.apply_safety_level or "safe-open-generate-supported",
1578
+ "inventory_supported": True,
1579
+ "edit_supported": True,
1580
+ "config_mutation_supported": True,
1581
+ "safe_open_generate_supported": True,
1582
+ "acceptance_verified": True,
1583
+ "donor_backed_ready": True,
1584
+ "requires_curated_donor": False,
1585
+ "requires_manual_acceptance": False,
1586
+ "donor_limited_generate": False,
1587
+ "recommended_donors": [selected_sample.relative_path],
1588
+ "known_limitations": [],
1589
+ }
1590
+ )
577
1591
  else:
578
1592
  unsupported_capabilities.append(capability)
579
1593
  capability_statuses.append(
@@ -587,8 +1601,10 @@ def build_coverage_gap_report(
587
1601
  "config_mutation_supported": False,
588
1602
  "safe_open_generate_supported": False,
589
1603
  "acceptance_verified": False,
1604
+ "donor_backed_ready": False,
590
1605
  "requires_curated_donor": False,
591
1606
  "requires_manual_acceptance": True,
1607
+ "donor_limited_generate": False,
592
1608
  "recommended_donors": [],
593
1609
  "known_limitations": ["no matching donor coverage found"],
594
1610
  }
@@ -603,6 +1619,7 @@ def build_coverage_gap_report(
603
1619
  "capability": str(status.get("capability")),
604
1620
  "inventory_supported": bool(status.get("inventory_supported")),
605
1621
  "edit_supported": bool(status.get("edit_supported")),
1622
+ "donor_backed_ready": _donor_backed_ready(status),
606
1623
  "generate_supported": bool(status.get("safe_open_generate_supported")),
607
1624
  "acceptance_verified": bool(status.get("acceptance_verified")),
608
1625
  "best_maturity_level": _best_maturity_level(status),
@@ -694,6 +1711,18 @@ def build_inventory_capability_report(payload: dict[str, Any]) -> dict[str, obje
694
1711
  capabilities.add("acl")
695
1712
  if payload.get("topology_summary", {}).get("network_style") == "wan_security":
696
1713
  capabilities.update({"vpn", "ipsec", "gre", "ppp"})
1714
+ for routing_details in payload.get("routing", {}).values():
1715
+ capabilities.update(str(capability) for capability in routing_details.get("capabilities", []) if str(capability).strip())
1716
+ for l2_details in payload.get("l2_security_monitoring", {}).values():
1717
+ capabilities.update(str(capability) for capability in l2_details.get("capabilities", []) if str(capability).strip())
1718
+ for l2_resiliency_details in payload.get("l2_resiliency_routing", {}).values():
1719
+ capabilities.update(str(capability) for capability in l2_resiliency_details.get("capabilities", []) if str(capability).strip())
1720
+ for ipv4_details in payload.get("ipv4_routing_management", {}).values():
1721
+ capabilities.update(str(capability) for capability in ipv4_details.get("capabilities", []) if str(capability).strip())
1722
+ for voice_details in payload.get("voice", {}).values():
1723
+ capabilities.update(str(capability) for capability in voice_details.get("capabilities", []) if str(capability).strip())
1724
+ for programming_details in payload.get("programming", {}).values():
1725
+ capabilities.update(str(capability) for capability in programming_details.get("feature_tags", []) if str(capability).strip())
697
1726
  return {
698
1727
  "device_families": families,
699
1728
  "capabilities": sorted(capabilities),