packet-tracer-skill 0.2.3 → 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 (53) hide show
  1. package/CHANGELOG.md +424 -73
  2. package/README.md +557 -442
  3. package/SKILL.md +337 -262
  4. package/bin/packet-tracer-skill.js +29 -2
  5. package/docs/github-launch-ops-0.2.3.md +37 -0
  6. package/docs/github-metadata.md +6 -4
  7. package/docs/hero-demo-plan.md +1 -1
  8. package/docs/home-iot-donor-proof.md +4 -4
  9. package/docs/l2-security-qos-proof.md +1 -1
  10. package/docs/packet-tracer-feature-gap-atlas.md +4 -4
  11. package/docs/post-launch-follow-up.md +9 -5
  12. package/docs/proof-readiness-dashboard.md +69 -0
  13. package/docs/publish-preview-roadmap.md +6 -5
  14. package/docs/release-checklist.md +17 -8
  15. package/docs/release-notes-0.2.4.md +20 -0
  16. package/docs/runtime-truth.md +33 -8
  17. package/docs/security-edge-deepening-proof.md +1 -1
  18. package/examples/README.md +98 -69
  19. package/examples/complex_campus_master_edit_v4.inventory.json +12 -2
  20. package/examples/gallery.md +94 -6
  21. package/examples/home_iot_cli_edit_v1.inventory.json +11 -2
  22. package/examples/index.json +932 -4
  23. package/examples/local-sample-evidence.json +24 -0
  24. package/examples/proof-cards.json +117 -0
  25. package/examples/service_heavy_cli_edit_v1.inventory.json +11 -2
  26. package/package.json +60 -53
  27. package/pytest.ini +9 -0
  28. package/references/packettracer-sample-catalog.json +45287 -4525
  29. package/references/packettracer-sample-catalog.md +599 -259
  30. package/references/proof-readiness-candidates.json +352 -0
  31. package/scripts/build_examples_index.py +228 -35
  32. package/scripts/build_sample_catalog.py +24 -44
  33. package/scripts/corpus_runner.py +430 -0
  34. package/scripts/coverage_matrix.py +1842 -1812
  35. package/scripts/donor_cache.py +354 -0
  36. package/scripts/donor_diagnostics.py +3 -1
  37. package/scripts/generate_pkt.py +8762 -4228
  38. package/scripts/intent_parser.py +2242 -1657
  39. package/scripts/local_donors.py +340 -0
  40. package/scripts/packet_tracer_env.py +846 -391
  41. package/scripts/pkt_annotate.py +218 -0
  42. package/scripts/pkt_codec.py +420 -181
  43. package/scripts/pkt_editor.py +2405 -1703
  44. package/scripts/pkt_transformer.py +1072 -727
  45. package/scripts/pkt_verify.py +461 -0
  46. package/scripts/runtime_doctor.py +80 -29
  47. package/scripts/sample_catalog.py +1372 -1250
  48. package/scripts/twofish_diagnostics.py +48 -31
  49. package/scripts/usage_ledger.py +218 -0
  50. package/scripts/vendor/README.md +44 -37
  51. package/scripts/vendor/twofish_pure.py +321 -0
  52. package/scripts/workspace_repair.py +548 -508
  53. package/templates/pt900/donors/README.md +15 -0
@@ -1,1812 +1,1842 @@
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
- def _scenario_generate_readiness(
1241
- scenario_family: str | None,
1242
- capability_statuses: list[dict[str, object]],
1243
- unsupported_capabilities: list[str],
1244
- requires_curated_donor: list[str],
1245
- requires_manual_acceptance: list[str],
1246
- ) -> dict[str, object]:
1247
- if not scenario_family:
1248
- return {
1249
- "family": None,
1250
- "status": "not_classified",
1251
- "critical_capabilities": [],
1252
- "missing_critical_capabilities": [],
1253
- "reasons": [],
1254
- }
1255
- critical_set = SCENARIO_CAPABILITY_SETS.get(scenario_family, set())
1256
- status_by_capability = {str(item.get("capability")): item for item in capability_statuses}
1257
- relevant_capabilities = [cap for cap in status_by_capability if cap in critical_set]
1258
- missing_critical = [cap for cap in unsupported_capabilities if cap in critical_set]
1259
- donor_limited_critical = [cap for cap in requires_curated_donor if cap in critical_set]
1260
- gated_critical = [cap for cap in requires_manual_acceptance if cap in critical_set and cap not in donor_limited_critical]
1261
- reasons: list[str] = []
1262
- if missing_critical:
1263
- reasons.append(f"missing critical capability coverage: {', '.join(missing_critical)}")
1264
- if donor_limited_critical:
1265
- reasons.append(f"critical capabilities depend on donor-limited safe-open coverage: {', '.join(donor_limited_critical)}")
1266
- if gated_critical:
1267
- reasons.append(f"critical capabilities remain acceptance-gated: {', '.join(gated_critical)}")
1268
- if missing_critical:
1269
- status = "unsupported"
1270
- elif donor_limited_critical:
1271
- status = "donor_limited"
1272
- elif gated_critical:
1273
- status = "acceptance_gated"
1274
- elif relevant_capabilities:
1275
- status = "ready"
1276
- else:
1277
- status = "partial"
1278
- reasons.append("scenario family is recognized, but no critical capability set was requested explicitly.")
1279
- return {
1280
- "family": scenario_family,
1281
- "status": status,
1282
- "critical_capabilities": relevant_capabilities,
1283
- "missing_critical_capabilities": missing_critical,
1284
- "reasons": reasons,
1285
- }
1286
-
1287
-
1288
- def _provider_families_for_capability(capability: str, requested_families: list[str]) -> set[str]:
1289
- mapped = CAPABILITY_PROVIDER_FAMILIES.get(capability)
1290
- if mapped:
1291
- return set(mapped)
1292
- return set(requested_families)
1293
-
1294
-
1295
- def _normalize_media_name(value: str) -> str:
1296
- media = value.strip().lower().replace("_", "-").replace(" ", "-")
1297
- aliases = {
1298
- "estraightthrough": "straight-through",
1299
- "copper-straight-through": "straight-through",
1300
- "straight-through": "straight-through",
1301
- "ecrossover": "crossover",
1302
- "copper-crossover": "crossover",
1303
- "crossover": "crossover",
1304
- "fiber": "fiber",
1305
- "efiber": "fiber",
1306
- "serial": "serial",
1307
- "eserialdce": "serial-dce",
1308
- "serial-dce": "serial-dce",
1309
- "eserialdte": "serial-dte",
1310
- "serial-dte": "serial-dte",
1311
- }
1312
- compact = media.replace("-", "")
1313
- return aliases.get(media, aliases.get(compact, media))
1314
-
1315
-
1316
- def _pair_key(left: str, right: str) -> str:
1317
- return " <-> ".join(sorted((left, right)))
1318
-
1319
-
1320
- def select_capability_matrix_hits(
1321
- plan: IntentPlan,
1322
- samples: list[SampleDescriptor],
1323
- ) -> list[CapabilityMatrixEntry]:
1324
- matrix = build_capability_matrix(samples)
1325
- requested_capabilities = set(plan.capabilities)
1326
- if not requested_capabilities:
1327
- return []
1328
- requested_families = _requested_device_families(plan)
1329
- hits: list[CapabilityMatrixEntry] = []
1330
- seen: set[tuple[str, str]] = set()
1331
- for entry in matrix:
1332
- if entry.capability not in requested_capabilities:
1333
- continue
1334
- provider_families = _provider_families_for_capability(entry.capability, requested_families)
1335
- if provider_families and entry.device_family not in provider_families:
1336
- continue
1337
- key = (entry.device_family, entry.capability)
1338
- if key in seen:
1339
- continue
1340
- hits.append(entry)
1341
- seen.add(key)
1342
- return hits
1343
-
1344
-
1345
- def build_blueprint_plan(plan: IntentPlan, blueprint: dict[str, object] | None = None) -> BlueprintPlan:
1346
- devices = [dict(device) for device in (blueprint or {}).get("devices", [])]
1347
- if not devices:
1348
- devices = [
1349
- {"type": device_type, "count": count}
1350
- for device_type, count in sorted(plan.device_requirements.items())
1351
- if count
1352
- ]
1353
- links = [dict(link) for link in (blueprint or {}).get("links", [])]
1354
- if not links:
1355
- links = [dict(link) for link in plan.links]
1356
- required_pairs: list[str] = []
1357
- for link in links:
1358
- left = str(link.get("a", {}).get("dev", ""))
1359
- right = str(link.get("b", {}).get("dev", ""))
1360
- if left and right:
1361
- required_pairs.append(" <-> ".join(sorted((left, right))))
1362
- return BlueprintPlan(
1363
- requested_devices=devices,
1364
- requested_links=links,
1365
- required_capabilities=sorted(dict.fromkeys(plan.capabilities)),
1366
- required_donor_capacity={key: value for key, value in sorted(plan.device_requirements.items()) if value},
1367
- required_link_pairs=sorted(dict.fromkeys(required_pairs)),
1368
- )
1369
-
1370
-
1371
- def build_coverage_gap_report(
1372
- plan: IntentPlan,
1373
- samples: list[SampleDescriptor],
1374
- *,
1375
- selected_donor: str | None = None,
1376
- ) -> CoverageGapReport:
1377
- iot_acceptance_gated = {"iot", "iot_registration", "iot_control"}
1378
- selected_sample = _selected_sample_for_donor(samples, selected_donor)
1379
- matrix = build_capability_matrix(samples)
1380
- by_capability: dict[str, list[CapabilityMatrixEntry]] = {}
1381
- by_family: dict[str, list[CapabilityMatrixEntry]] = {}
1382
- for entry in matrix:
1383
- by_capability.setdefault(entry.capability, []).append(entry)
1384
- by_family.setdefault(entry.device_family, []).append(entry)
1385
-
1386
- requested_families = _requested_device_families(plan)
1387
- requested_capabilities = sorted(dict.fromkeys(plan.capabilities))
1388
- supported_capabilities: list[str] = []
1389
- unsupported_capabilities: list[str] = []
1390
- requires_curated_donor: list[str] = []
1391
- requires_manual_acceptance: list[str] = []
1392
- capability_statuses: list[dict[str, object]] = []
1393
- for capability in requested_capabilities:
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
1468
- matching_entries = [
1469
- entry
1470
- for entry in by_capability.get(capability, [])
1471
- if not provider_families or entry.device_family in provider_families
1472
- ]
1473
- if matching_entries:
1474
- supported_capabilities.append(capability)
1475
- best_entry = max(matching_entries, key=lambda entry: MATURITY_RANK[entry.maturity_level])
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:
1488
- requires_curated_donor.append(capability)
1489
- if not acceptance_verified:
1490
- requires_manual_acceptance.append(capability)
1491
- capability_statuses.append(
1492
- {
1493
- "capability": capability,
1494
- "provider_families": sorted(provider_families),
1495
- "matching_device_families": sorted({entry.device_family for entry in matching_entries}),
1496
- "best_maturity_level": "inventory-supported" if report_only else "config-mutation-supported" if edit_proven_only else best_entry.maturity_level,
1497
- "inventory_supported": any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["inventory-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),
1501
- "acceptance_verified": acceptance_verified,
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,
1511
- "requires_manual_acceptance": not acceptance_verified,
1512
- "report_only": report_only,
1513
- "donor_limited_generate": wan_security_donor_limited,
1514
- "recommended_donors": list(dict.fromkeys(donor for entry in matching_entries for donor in entry.accepted_donors))[:10],
1515
- "known_limitations": list(dict.fromkeys(item for entry in matching_entries for item in entry.known_limitations)),
1516
- }
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
- )
1591
- else:
1592
- unsupported_capabilities.append(capability)
1593
- capability_statuses.append(
1594
- {
1595
- "capability": capability,
1596
- "provider_families": sorted(provider_families),
1597
- "matching_device_families": [],
1598
- "best_maturity_level": "reference-known",
1599
- "inventory_supported": False,
1600
- "edit_supported": False,
1601
- "config_mutation_supported": False,
1602
- "safe_open_generate_supported": False,
1603
- "acceptance_verified": False,
1604
- "donor_backed_ready": False,
1605
- "requires_curated_donor": False,
1606
- "requires_manual_acceptance": True,
1607
- "donor_limited_generate": False,
1608
- "recommended_donors": [],
1609
- "known_limitations": ["no matching donor coverage found"],
1610
- }
1611
- )
1612
-
1613
- unsupported_families = [family for family in requested_families if family not in by_family]
1614
-
1615
- requires_curated_donor = sorted(dict.fromkeys(requires_curated_donor))
1616
- requires_manual_acceptance = sorted(dict.fromkeys(requires_manual_acceptance))
1617
- capability_parity = [
1618
- {
1619
- "capability": str(status.get("capability")),
1620
- "inventory_supported": bool(status.get("inventory_supported")),
1621
- "edit_supported": bool(status.get("edit_supported")),
1622
- "donor_backed_ready": _donor_backed_ready(status),
1623
- "generate_supported": bool(status.get("safe_open_generate_supported")),
1624
- "acceptance_verified": bool(status.get("acceptance_verified")),
1625
- "best_maturity_level": _best_maturity_level(status),
1626
- "generate_mismatch_reason": _generate_mismatch_reason(status),
1627
- "required_donor_families": sorted(_provider_families_for_capability(str(status.get("capability")), requested_families)),
1628
- "required_archetypes": CAPABILITY_REQUIRED_ARCHETYPES.get(str(status.get("capability")), []),
1629
- "required_runtime_features": CAPABILITY_REQUIRED_RUNTIME_FEATURES.get(str(status.get("capability")), []),
1630
- "recommended_next_action": _recommended_next_action_for_capability(
1631
- str(status.get("capability")),
1632
- _generate_mismatch_reason(status),
1633
- ),
1634
- }
1635
- for status in capability_statuses
1636
- ]
1637
- scenario_family = _scenario_family_for_plan(plan, requested_families)
1638
- if selected_donor is None and plan.blocked_mutations:
1639
- requires_manual_acceptance = sorted(dict.fromkeys([*requires_manual_acceptance, *plan.blocked_mutations]))
1640
- scenario_generate_readiness = _scenario_generate_readiness(
1641
- scenario_family,
1642
- capability_statuses,
1643
- unsupported_capabilities,
1644
- requires_curated_donor,
1645
- requires_manual_acceptance,
1646
- )
1647
- recommended_next_actions: list[str] = []
1648
- if unsupported_capabilities or unsupported_families:
1649
- recommended_next_actions.append(
1650
- "Inspect the blueprint and import or provide a donor that contains the missing capabilities or device families."
1651
- )
1652
- if requires_manual_acceptance:
1653
- recommended_next_actions.append(
1654
- "Run explain-plan or validate-open-debug before enabling the acceptance-gated capability set."
1655
- )
1656
- if not requested_capabilities and not requested_families:
1657
- recommended_next_actions.append("Add explicit device, service, or topology requirements so donor ranking can be constrained.")
1658
-
1659
- return CoverageGapReport(
1660
- requested_capabilities=requested_capabilities,
1661
- supported_capabilities=supported_capabilities,
1662
- unsupported_capabilities=unsupported_capabilities,
1663
- requested_device_families=requested_families,
1664
- unsupported_device_families=unsupported_families,
1665
- requires_curated_donor=requires_curated_donor,
1666
- requires_manual_acceptance=requires_manual_acceptance,
1667
- capability_statuses=capability_statuses,
1668
- capability_parity=capability_parity,
1669
- scenario_family=scenario_family,
1670
- scenario_generate_readiness=scenario_generate_readiness,
1671
- recommended_next_actions=recommended_next_actions,
1672
- )
1673
-
1674
-
1675
- def build_inventory_capability_report(payload: dict[str, Any]) -> dict[str, object]:
1676
- device_counts = dict(payload.get("topology_summary", {}).get("device_counts", {}))
1677
- families = sorted({_device_family_for_type(device_type) for device_type in device_counts})
1678
- capabilities: set[str] = set()
1679
- if "multilayer switches" in families:
1680
- capabilities.add("multilayer_switching")
1681
- if "wan/cloud/dsl/cable devices" in families:
1682
- capabilities.add("wan")
1683
- if "security devices" in families:
1684
- capabilities.add("security_edge")
1685
- if payload.get("vlans"):
1686
- capabilities.update(["vlan", "trunk", "access_port"])
1687
- if payload.get("dhcp_pools"):
1688
- capabilities.update(["dhcp_pool", "router_dhcp", "server_dhcp"])
1689
- if payload.get("services"):
1690
- capabilities.update(
1691
- {
1692
- "dns" if "dns_server" in service else service
1693
- for service_names in payload["services"].values()
1694
- for service in service_names
1695
- }
1696
- )
1697
- if payload.get("wireless"):
1698
- capabilities.update(["wireless_ap", "wireless_mutation", "wireless_client"])
1699
- if any(family == "end devices" for family in families):
1700
- capabilities.add("wireless_client_association")
1701
- if payload.get("iot"):
1702
- capabilities.add("iot")
1703
- iot_roles = {str(entry.get("role", "")).strip() for entry in payload["iot"].values() if str(entry.get("role", "")).strip()}
1704
- if "thing" in iot_roles and ("gateway" in iot_roles or "server" in iot_roles):
1705
- capabilities.add("iot_registration")
1706
- if "thing" in iot_roles and ("gateway" in iot_roles or "server" in iot_roles):
1707
- capabilities.add("iot_control")
1708
- if any(family == "end devices" for family in families):
1709
- capabilities.add("end_device_mutation")
1710
- if payload.get("acl_names"):
1711
- capabilities.add("acl")
1712
- if payload.get("topology_summary", {}).get("network_style") == "wan_security":
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())
1726
- return {
1727
- "device_families": families,
1728
- "capabilities": sorted(capabilities),
1729
- "device_counts": device_counts,
1730
- }
1731
-
1732
-
1733
- def build_donor_graph_fit(sample: SampleDescriptor, blueprint: dict[str, object] | None = None) -> DonorGraphFit:
1734
- expected_pairs: list[str] = []
1735
- expected_media: dict[str, str] = {}
1736
- expected_ports: dict[str, set[str]] = {}
1737
- blueprint_archetype = ""
1738
- blueprint_device_count = 0
1739
- if blueprint:
1740
- blueprint_archetype = str(blueprint.get("topology_archetype", "")).strip()
1741
- blueprint_device_count = len(list(blueprint.get("devices", [])))
1742
- for link in blueprint.get("links", []):
1743
- left = str(link.get("a", {}).get("dev", ""))
1744
- right = str(link.get("b", {}).get("dev", ""))
1745
- if left and right:
1746
- pair = _pair_key(left, right)
1747
- expected_pairs.append(pair)
1748
- media = _normalize_media_name(str(link.get("media", "")))
1749
- if media:
1750
- expected_media[pair] = media
1751
- ports = {
1752
- str(link.get("a", {}).get("port", "")).strip(),
1753
- str(link.get("b", {}).get("port", "")).strip(),
1754
- }
1755
- expected_ports[pair] = {port for port in ports if port}
1756
- sample_pairs: set[str] = set()
1757
- sample_media: dict[str, str] = {}
1758
- sample_ports: dict[str, set[str]] = {}
1759
- for link in sample.links:
1760
- left = str(link.get("from", "")).strip()
1761
- right = str(link.get("to", "")).strip()
1762
- if left and right:
1763
- pair = _pair_key(left, right)
1764
- sample_pairs.add(pair)
1765
- media = _normalize_media_name(str(link.get("media", "") or link.get("cable_type", "")))
1766
- if media and pair not in sample_media:
1767
- sample_media[pair] = media
1768
- ports = {str(port).strip() for port in link.get("ports", []) if str(port).strip()}
1769
- if ports:
1770
- sample_ports.setdefault(pair, set()).update(ports)
1771
- matched_pairs = [pair for pair in expected_pairs if pair in sample_pairs]
1772
- missing_pairs = [pair for pair in expected_pairs if pair not in sample_pairs]
1773
- port_media_conflicts: list[str] = []
1774
- for pair in matched_pairs:
1775
- expected_pair_media = expected_media.get(pair)
1776
- sample_pair_media = sample_media.get(pair)
1777
- if expected_pair_media and sample_pair_media and expected_pair_media != sample_pair_media:
1778
- port_media_conflicts.append(f"{pair} media mismatch ({expected_pair_media} != {sample_pair_media})")
1779
- expected_pair_ports = expected_ports.get(pair, set())
1780
- sample_pair_ports = sample_ports.get(pair, set())
1781
- if expected_pair_ports and sample_pair_ports and not expected_pair_ports.issubset(sample_pair_ports):
1782
- port_media_conflicts.append(
1783
- f"{pair} port mismatch (expected {sorted(expected_pair_ports)}, available {sorted(sample_pair_ports)})"
1784
- )
1785
- fit_score = max(0, len(matched_pairs) * 10 - len(missing_pairs) * 5 - len(port_media_conflicts) * 3)
1786
- topology_bonus = 0
1787
- if blueprint_archetype:
1788
- sample_topology = set(sample.topology_tags)
1789
- if blueprint_archetype in sample_topology:
1790
- topology_bonus += 18
1791
- elif blueprint_archetype == "chain" and sample_topology & {"department_lan", "core_access"}:
1792
- topology_bonus += 12
1793
- elif blueprint_archetype == "core_access" and sample_topology & {"department_lan", "router_on_a_stick"}:
1794
- topology_bonus += 10
1795
- elif blueprint_archetype == "wireless_branch" and sample_topology & {"wireless_edge", "small_office"}:
1796
- topology_bonus += 10
1797
- capacity_penalty = max(0, blueprint_device_count - sample.device_count) * 2
1798
- layout_reuse_score = max(
1799
- 0,
1800
- len(matched_pairs) * 12 + topology_bonus - len(missing_pairs) * 6 - len(port_media_conflicts) * 4 - capacity_penalty,
1801
- )
1802
- return DonorGraphFit(
1803
- matched_pairs=matched_pairs,
1804
- missing_pairs=missing_pairs,
1805
- port_media_conflicts=port_media_conflicts,
1806
- fit_score=fit_score,
1807
- layout_reuse_score=layout_reuse_score,
1808
- )
1809
-
1810
-
1811
- def asdict_list(items: list[Any]) -> list[dict[str, object]]:
1812
- 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]