packet-tracer-skill 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -2
- package/README.md +443 -124
- package/docs/automation-controller-proof.md +35 -0
- package/docs/campus-donor-proof.md +103 -0
- package/docs/curated-donor-registry.md +37 -0
- package/docs/generate-ready-pilot-design.md +30 -0
- package/docs/github-launch-ops-0.2.1.md +45 -0
- package/docs/github-launch-ops-0.2.2.md +42 -0
- package/docs/github-metadata.md +23 -6
- package/docs/home-iot-donor-proof.md +64 -0
- package/docs/industrial-programming-proof.md +48 -0
- package/docs/ipv4-routing-management-proof.md +37 -0
- package/docs/l2-resiliency-bgp-proof.md +60 -0
- package/docs/l2-security-qos-proof.md +59 -0
- package/docs/launch-announcement-0.2.2.md +15 -0
- package/docs/packet-tracer-feature-gap-atlas.md +244 -0
- package/docs/post-launch-follow-up.md +39 -0
- package/docs/publish-preview-roadmap.md +20 -17
- package/docs/release-checklist.md +32 -17
- package/docs/release-notes-0.2.1.md +7 -6
- package/docs/release-notes-0.2.2.md +26 -0
- package/docs/release-notes-0.2.3.md +59 -0
- package/docs/runtime-truth.md +31 -0
- package/docs/security-edge-deepening-proof.md +65 -0
- package/docs/voice-collaboration-proof.md +38 -0
- package/docs/wan-security-donor-proof.md +58 -0
- package/docs/wireless-advanced-proof.md +60 -0
- package/examples/README.md +11 -8
- package/examples/gallery.md +9 -3
- package/examples/index.json +3 -3
- package/package.json +20 -1
- package/references/curated-donor-registry.json +5 -2
- package/references/packettracer-feature-atlas.json +183 -0
- package/references/scenario-fixture-corpus.json +1 -0
- package/scripts/build_examples_index.py +26 -15
- package/scripts/coverage_matrix.py +1054 -25
- package/scripts/feature_atlas.py +293 -0
- package/scripts/generate_pkt.py +491 -34
- package/scripts/intent_parser.py +798 -8
- package/scripts/pkt_editor.py +651 -0
- package/scripts/remote_search.py +197 -21
- package/scripts/runtime_doctor.py +83 -10
- package/scripts/sample_catalog.py +266 -6
package/scripts/generate_pkt.py
CHANGED
|
@@ -4,7 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import copy
|
|
7
|
+
from collections import Counter, defaultdict
|
|
7
8
|
from dataclasses import asdict, dataclass, field
|
|
9
|
+
from functools import lru_cache
|
|
8
10
|
import json
|
|
9
11
|
import os
|
|
10
12
|
from pathlib import Path
|
|
@@ -25,6 +27,7 @@ from coverage_matrix import (
|
|
|
25
27
|
build_inventory_capability_report,
|
|
26
28
|
select_capability_matrix_hits,
|
|
27
29
|
)
|
|
30
|
+
from feature_atlas import build_feature_gap_report
|
|
28
31
|
from intent_parser import IntentPlan, parse_intent
|
|
29
32
|
from packet_tracer_env import (
|
|
30
33
|
get_packet_tracer_compatibility_donor,
|
|
@@ -35,7 +38,13 @@ from pkt_builder import build_packet_tracer_xml
|
|
|
35
38
|
from pkt_codec import decode_pkt_file, decode_pkt_modern, encode_pkt_modern
|
|
36
39
|
from pkt_editor import apply_plan_operations, decode_pkt_to_root, edit_pkt_file, inventory_devices, inventory_links, inventory_root
|
|
37
40
|
from pkt_transformer import transform_from_blueprint
|
|
38
|
-
from remote_search import
|
|
41
|
+
from remote_search import (
|
|
42
|
+
asdict_list as remote_asdict_list,
|
|
43
|
+
auto_import_remote_candidates,
|
|
44
|
+
candidate_is_curated_eligible,
|
|
45
|
+
search_remote_candidates,
|
|
46
|
+
write_remote_sample_audit,
|
|
47
|
+
)
|
|
39
48
|
from sample_catalog import ReferencePattern, SampleCandidate, SampleDescriptor, load_catalog, load_curated_donor_catalog, load_reference_catalog, summarize_pkt_descriptor
|
|
40
49
|
from sample_selector import rank_curated_donor_samples, rank_reference_samples, rank_samples, select_best_sample
|
|
41
50
|
from workspace_repair import inspect_donor_coherence, inspect_workspace_integrity, validate_donor_coherence, validate_workspace_integrity
|
|
@@ -81,6 +90,9 @@ DEVICE_FAMILY_MAP = {
|
|
|
81
90
|
"Tablet": "end devices",
|
|
82
91
|
"Smartphone": "end devices",
|
|
83
92
|
"Printer": "end devices",
|
|
93
|
+
"IpPhone": "end devices",
|
|
94
|
+
"HomeVoip": "end devices",
|
|
95
|
+
"AnalogPhone": "end devices",
|
|
84
96
|
"LightWeightAccessPoint": "access points",
|
|
85
97
|
"WirelessRouter": "home/wireless routers",
|
|
86
98
|
"WirelessRouterNewGeneration": "home/wireless routers",
|
|
@@ -107,6 +119,11 @@ if hasattr(sys.stdout, "reconfigure"):
|
|
|
107
119
|
pass
|
|
108
120
|
|
|
109
121
|
|
|
122
|
+
@lru_cache(maxsize=1)
|
|
123
|
+
def _inspect_packet_tracer_compatibility_donor_cached():
|
|
124
|
+
return inspect_packet_tracer_compatibility_donor()
|
|
125
|
+
|
|
126
|
+
|
|
110
127
|
class PlanningError(RuntimeError):
|
|
111
128
|
def __init__(self, message: str, plan: IntentPlan) -> None:
|
|
112
129
|
super().__init__(message)
|
|
@@ -139,7 +156,7 @@ STRICT_COMPATIBILITY_GAP = (
|
|
|
139
156
|
|
|
140
157
|
|
|
141
158
|
def _compat_donor_details() -> tuple[Path | None, str | None]:
|
|
142
|
-
details =
|
|
159
|
+
details = _inspect_packet_tracer_compatibility_donor_cached()
|
|
143
160
|
return details.resolved_path, details.donor_version
|
|
144
161
|
|
|
145
162
|
|
|
@@ -244,6 +261,8 @@ def _resolve_remote_sources(
|
|
|
244
261
|
remote_provider: str = "github",
|
|
245
262
|
import_cache_root: Path | None = None,
|
|
246
263
|
max_remote_results: int = 10,
|
|
264
|
+
remote_dry_run: bool = False,
|
|
265
|
+
remote_audit_out: Path | None = None,
|
|
247
266
|
) -> tuple[list[Path], list[Path], list[dict[str, object]]]:
|
|
248
267
|
resolved_reference_roots = list(reference_roots or [])
|
|
249
268
|
resolved_donor_roots = list(donor_roots or [])
|
|
@@ -251,13 +270,19 @@ def _resolve_remote_sources(
|
|
|
251
270
|
return resolved_reference_roots, resolved_donor_roots, []
|
|
252
271
|
remote_candidates = search_remote_candidates(plan, provider=remote_provider, max_results=max_remote_results)
|
|
253
272
|
cache_root = import_cache_root or _default_import_cache_root()
|
|
254
|
-
imported_candidates = auto_import_remote_candidates(
|
|
273
|
+
imported_candidates = auto_import_remote_candidates(
|
|
274
|
+
remote_candidates,
|
|
275
|
+
cache_root,
|
|
276
|
+
max_results=max_remote_results,
|
|
277
|
+
dry_run=remote_dry_run,
|
|
278
|
+
)
|
|
279
|
+
write_remote_sample_audit(imported_candidates, cache_root, remote_audit_out)
|
|
255
280
|
for candidate in imported_candidates:
|
|
256
281
|
if candidate.path:
|
|
257
282
|
imported_root = Path(candidate.path)
|
|
258
283
|
if imported_root not in resolved_reference_roots:
|
|
259
284
|
resolved_reference_roots.append(imported_root)
|
|
260
|
-
if imported_root not in resolved_donor_roots:
|
|
285
|
+
if candidate_is_curated_eligible(candidate) and imported_root not in resolved_donor_roots:
|
|
261
286
|
resolved_donor_roots.append(imported_root)
|
|
262
287
|
return resolved_reference_roots, resolved_donor_roots, remote_asdict_list(imported_candidates)
|
|
263
288
|
|
|
@@ -327,28 +352,100 @@ def _preferred_donor_archetypes_for_plan(plan: IntentPlan, topology_tags: list[s
|
|
|
327
352
|
}
|
|
328
353
|
capabilities = set(plan.capabilities)
|
|
329
354
|
tags = set(topology_tags or [])
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
"
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
355
|
+
prompt_lower = str(plan.prompt or "").lower()
|
|
356
|
+
explicit_home_gateway_prompt = any(
|
|
357
|
+
token in prompt_lower
|
|
358
|
+
for token in ["home gateway", "homegateway", "smart home", "iot "]
|
|
359
|
+
)
|
|
360
|
+
family_hints: list[str] = []
|
|
361
|
+
campus_signal = (
|
|
362
|
+
plan.network_style == "campus"
|
|
363
|
+
or "campus" in prompt_lower
|
|
364
|
+
or "kampus" in prompt_lower
|
|
365
|
+
or bool(plan.department_groups)
|
|
366
|
+
or bool(tags & {"chain", "core_access", "router_on_a_stick", "acl_policy"})
|
|
367
|
+
or bool(
|
|
368
|
+
device_families & {"routers", "switches", "multilayer switches"}
|
|
369
|
+
and capabilities & {"vlan", "router_on_a_stick", "management_vlan", "telnet", "acl", "trunk", "access_port"}
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
home_iot_signal = (
|
|
373
|
+
plan.network_style in {"home_iot", "smart_home"}
|
|
374
|
+
or explicit_home_gateway_prompt
|
|
375
|
+
or plan.wireless_mode == "home_router_edge"
|
|
376
|
+
or bool(capabilities & {"iot", "iot_registration", "iot_control"})
|
|
377
|
+
or bool("iot devices" in device_families)
|
|
378
|
+
)
|
|
379
|
+
wan_signal = (
|
|
380
|
+
plan.network_style == "wan_security"
|
|
381
|
+
or bool(device_families & {"wan/cloud/dsl/cable devices", "security devices"})
|
|
382
|
+
or bool(capabilities & {"vpn", "ipsec", "gre", "ppp", "multilayer_switching", "security_edge"})
|
|
383
|
+
)
|
|
384
|
+
ipv6_routing_signal = plan.network_style == "ipv6_routing" or bool(capabilities & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ipv6_prefix_delegation", "ipv6_dns_aaaa", "ipv6_tunneling", "isatap", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"})
|
|
385
|
+
ipv4_routing_management_signal = plan.network_style == "ipv4_routing_management" or bool(capabilities & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"})
|
|
386
|
+
l2_security_monitoring_signal = plan.network_style == "l2_security_monitoring" or bool(capabilities & {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"})
|
|
387
|
+
wireless_advanced_signal = plan.network_style == "wireless_advanced" or bool(capabilities & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"})
|
|
388
|
+
automation_controller_signal = plan.network_style == "automation_controller" or bool(capabilities & {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"})
|
|
389
|
+
voice_collaboration_signal = plan.network_style == "voice_collaboration" or bool(capabilities & {"voip", "ip_phone", "call_manager", "linksys_voice"})
|
|
390
|
+
industrial_iot_signal = plan.network_style == "industrial_iot" or bool(capabilities & {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"})
|
|
391
|
+
wireless_signal = plan.wireless_mode or capabilities & {
|
|
342
392
|
"wireless_ap",
|
|
343
393
|
"wireless_client",
|
|
344
394
|
"wireless_mutation",
|
|
345
395
|
"wireless_client_association",
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
396
|
+
}
|
|
397
|
+
service_signal = (
|
|
398
|
+
requested_services & {"dns", "dhcp", "http", "https", "ftp", "tftp", "email", "syslog", "aaa", "ntp"}
|
|
399
|
+
or capabilities & {"server_dns", "server_dhcp", "server_http", "server_https", "server_ftp", "server_tftp", "server_email", "server_syslog", "server_aaa"}
|
|
400
|
+
or "servers" in device_families
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if industrial_iot_signal:
|
|
404
|
+
family_hints.append("industrial IoT")
|
|
405
|
+
elif automation_controller_signal:
|
|
406
|
+
family_hints.append("automation/controller")
|
|
407
|
+
elif voice_collaboration_signal:
|
|
408
|
+
family_hints.append("voice/collaboration")
|
|
409
|
+
elif wireless_advanced_signal:
|
|
410
|
+
family_hints.append("advanced wireless")
|
|
411
|
+
elif l2_security_monitoring_signal:
|
|
412
|
+
family_hints.append("L2 security/monitoring")
|
|
413
|
+
elif ipv6_routing_signal:
|
|
414
|
+
family_hints.append("IPv6/routing")
|
|
415
|
+
elif ipv4_routing_management_signal:
|
|
416
|
+
family_hints.append("IPv4 routing/management")
|
|
417
|
+
elif wan_signal:
|
|
418
|
+
family_hints.append("WAN/security edge")
|
|
419
|
+
if campus_signal and not bool(capabilities & {"vpn", "ipsec", "gre", "ppp", "security_edge"}):
|
|
420
|
+
family_hints.append("campus/core")
|
|
421
|
+
elif campus_signal:
|
|
422
|
+
family_hints.append("campus/core")
|
|
423
|
+
if wireless_signal:
|
|
424
|
+
family_hints.append("wireless-heavy")
|
|
425
|
+
if service_signal and requested_services & {"email", "syslog", "aaa", "http", "https", "ftp", "tftp"}:
|
|
426
|
+
family_hints.append("service-heavy")
|
|
427
|
+
elif home_iot_signal:
|
|
428
|
+
family_hints.append("IoT/home gateway")
|
|
429
|
+
if wireless_signal:
|
|
430
|
+
family_hints.append("wireless-heavy")
|
|
431
|
+
elif service_signal:
|
|
432
|
+
family_hints.append("service-heavy")
|
|
433
|
+
elif wireless_signal:
|
|
434
|
+
family_hints.append("wireless-heavy")
|
|
435
|
+
|
|
436
|
+
if not family_hints:
|
|
437
|
+
if service_signal:
|
|
438
|
+
family_hints.append("service-heavy")
|
|
439
|
+
if wireless_signal:
|
|
440
|
+
family_hints.append("wireless-heavy")
|
|
441
|
+
if home_iot_signal:
|
|
442
|
+
family_hints.append("IoT/home gateway")
|
|
443
|
+
if wan_signal:
|
|
444
|
+
family_hints.append("WAN/security edge")
|
|
445
|
+
|
|
446
|
+
for item in family_hints:
|
|
447
|
+
if item not in preferred:
|
|
448
|
+
preferred.append(item)
|
|
352
449
|
return preferred
|
|
353
450
|
|
|
354
451
|
|
|
@@ -361,6 +458,18 @@ def _required_runtime_features_for_plan(plan: IntentPlan) -> list[str]:
|
|
|
361
458
|
features.add("wireless_runtime")
|
|
362
459
|
if capabilities & {"iot", "iot_registration", "iot_control"}:
|
|
363
460
|
features.add("iot_runtime")
|
|
461
|
+
if capabilities & {"vpn", "ipsec", "gre"}:
|
|
462
|
+
features.add("tunnel_runtime")
|
|
463
|
+
if capabilities & {"ppp"}:
|
|
464
|
+
features.add("wan_runtime")
|
|
465
|
+
if capabilities & {"security_edge", "acl", "nat"}:
|
|
466
|
+
features.add("security_runtime")
|
|
467
|
+
if capabilities & {"multilayer_switching"}:
|
|
468
|
+
features.add("multilayer_runtime")
|
|
469
|
+
if capabilities & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}:
|
|
470
|
+
features.add("ipv6_runtime")
|
|
471
|
+
if capabilities & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"}:
|
|
472
|
+
features.add("ipv4_routing_management_runtime")
|
|
364
473
|
return sorted(features)
|
|
365
474
|
|
|
366
475
|
|
|
@@ -437,6 +546,20 @@ def _candidate_acceptance_penalty(candidate: SampleCandidate, blueprint: dict[st
|
|
|
437
546
|
"wireless_mutation": 18,
|
|
438
547
|
"wireless_client_association": 22,
|
|
439
548
|
"end_device_mutation": 12,
|
|
549
|
+
"iot_registration": 26,
|
|
550
|
+
"iot_control": 24,
|
|
551
|
+
"vpn": 22,
|
|
552
|
+
"ipsec": 22,
|
|
553
|
+
"gre": 20,
|
|
554
|
+
"ppp": 18,
|
|
555
|
+
"security_edge": 20,
|
|
556
|
+
"multilayer_switching": 16,
|
|
557
|
+
"ipv6_slaac": 18,
|
|
558
|
+
"dhcpv6_stateful": 18,
|
|
559
|
+
"ospfv3": 18,
|
|
560
|
+
"eigrp_ipv6": 18,
|
|
561
|
+
"ripng": 16,
|
|
562
|
+
"hsrp": 16,
|
|
440
563
|
}.items():
|
|
441
564
|
if capability in requested_capabilities and capability not in supported_capabilities:
|
|
442
565
|
penalty += penalty_value
|
|
@@ -507,15 +630,110 @@ def _summarize_candidate_pool(
|
|
|
507
630
|
break
|
|
508
631
|
if len(top_rejection_reasons) >= 5:
|
|
509
632
|
continue
|
|
633
|
+
primary_rejection_code = _primary_rejection_code(top_rejection_reasons)
|
|
634
|
+
primary_rejection_layer = _primary_rejection_layer(primary_rejection_code)
|
|
635
|
+
best_rejected_donor_class = (
|
|
636
|
+
next((str(item) for item in list(preferred_archetypes or []) if str(item).strip()), None)
|
|
637
|
+
if any(counts[key] > 0 for key in ("rejected", "filtered"))
|
|
638
|
+
else None
|
|
639
|
+
)
|
|
510
640
|
return {
|
|
511
641
|
"preferred_donor_archetypes": list(preferred_archetypes or []),
|
|
512
642
|
"candidate_counts": counts,
|
|
513
643
|
"best_adjusted_total_score": best_adjusted_score,
|
|
514
644
|
"best_layout_reuse_score": best_layout_reuse_score,
|
|
515
645
|
"top_rejection_reasons": top_rejection_reasons,
|
|
646
|
+
"best_rejected_donor_class": best_rejected_donor_class,
|
|
647
|
+
"primary_rejection_code": primary_rejection_code,
|
|
648
|
+
"primary_rejection_layer": primary_rejection_layer,
|
|
516
649
|
}
|
|
517
650
|
|
|
518
651
|
|
|
652
|
+
def _primary_rejection_code(rejection_reasons: list[str]) -> str | None:
|
|
653
|
+
normalized = [str(item).strip().lower() for item in rejection_reasons if str(item).strip()]
|
|
654
|
+
if any("runtime subtree" in reason for reason in normalized):
|
|
655
|
+
return "runtime_subtree_missing"
|
|
656
|
+
if any("archetype_gap:" in reason or "archetype does not align" in reason for reason in normalized):
|
|
657
|
+
return "archetype_misaligned"
|
|
658
|
+
if any(
|
|
659
|
+
marker in reason
|
|
660
|
+
for reason in normalized
|
|
661
|
+
for marker in (
|
|
662
|
+
"missing_link_pairs:",
|
|
663
|
+
"no reusable link pairs",
|
|
664
|
+
"reuses too little of the requested link skeleton",
|
|
665
|
+
"cannot create new donor link pair",
|
|
666
|
+
"requires donor link reuse",
|
|
667
|
+
"port mismatch",
|
|
668
|
+
"media mismatch",
|
|
669
|
+
"ports/media",
|
|
670
|
+
)
|
|
671
|
+
):
|
|
672
|
+
return "layout_reuse_too_weak"
|
|
673
|
+
if any(
|
|
674
|
+
marker in reason
|
|
675
|
+
for reason in normalized
|
|
676
|
+
for marker in (
|
|
677
|
+
"acceptance penalty",
|
|
678
|
+
"acceptance_gated",
|
|
679
|
+
"acceptance-gated",
|
|
680
|
+
"acceptance fixture",
|
|
681
|
+
"acceptance evidence",
|
|
682
|
+
"acceptance risk",
|
|
683
|
+
)
|
|
684
|
+
):
|
|
685
|
+
return "acceptance_evidence_too_weak"
|
|
686
|
+
return None
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _primary_rejection_layer(primary_rejection_code: str | None) -> str | None:
|
|
690
|
+
if primary_rejection_code == "acceptance_evidence_too_weak":
|
|
691
|
+
return "acceptance"
|
|
692
|
+
if primary_rejection_code in {
|
|
693
|
+
"layout_reuse_too_weak",
|
|
694
|
+
"archetype_misaligned",
|
|
695
|
+
"runtime_subtree_missing",
|
|
696
|
+
}:
|
|
697
|
+
return "donor"
|
|
698
|
+
return None
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _best_rejected_donor_summary(
|
|
702
|
+
best_rejected_donor_class: str | None,
|
|
703
|
+
primary_rejection_code: str | None,
|
|
704
|
+
top_rejection_reasons: list[str],
|
|
705
|
+
) -> str | None:
|
|
706
|
+
if not best_rejected_donor_class and not primary_rejection_code:
|
|
707
|
+
return None
|
|
708
|
+
reason_sentence_map = {
|
|
709
|
+
"layout_reuse_too_weak": "its reusable link skeleton is still too weak for the requested topology",
|
|
710
|
+
"acceptance_evidence_too_weak": "its acceptance evidence is still too weak for prompt-level generate",
|
|
711
|
+
"archetype_misaligned": "its donor shape does not align with the requested scenario archetype",
|
|
712
|
+
"runtime_subtree_missing": "the required runtime subtree is missing from the donor path",
|
|
713
|
+
}
|
|
714
|
+
next_shape_map = {
|
|
715
|
+
"layout_reuse_too_weak": "choose a donor with the same family but a stronger reusable router-switch-service skeleton",
|
|
716
|
+
"acceptance_evidence_too_weak": "choose a donor with explicit acceptance-backed coverage for the same scenario family",
|
|
717
|
+
"archetype_misaligned": "choose a donor whose archetype already matches the requested family",
|
|
718
|
+
"runtime_subtree_missing": "choose a donor that already contains the required runtime subtree and open-state structure",
|
|
719
|
+
}
|
|
720
|
+
donor_class = best_rejected_donor_class or "the closest donor class"
|
|
721
|
+
reason = reason_sentence_map.get(primary_rejection_code or "", "it still did not satisfy strict donor selection")
|
|
722
|
+
next_shape = next_shape_map.get(primary_rejection_code or "", "choose a closer donor before prompt generate")
|
|
723
|
+
if donor_class == "WAN/security edge":
|
|
724
|
+
wan_next_shape_map = {
|
|
725
|
+
"layout_reuse_too_weak": "choose a WAN/security donor with reusable ASA/cloud/serial or tunnel skeleton",
|
|
726
|
+
"acceptance_evidence_too_weak": "choose a WAN/security donor with explicit VPN/IPSec/GRE/PPP acceptance evidence",
|
|
727
|
+
"archetype_misaligned": "choose a donor already tagged as WAN/security edge",
|
|
728
|
+
"runtime_subtree_missing": "choose a donor that already contains the WAN/security runtime subtree",
|
|
729
|
+
}
|
|
730
|
+
next_shape = wan_next_shape_map.get(primary_rejection_code or "", "choose a closer WAN/security donor before prompt generate")
|
|
731
|
+
summary = f"Best rejected donor class {donor_class} was closest, but {reason}; {next_shape}."
|
|
732
|
+
if top_rejection_reasons:
|
|
733
|
+
summary = f"{summary} Top rejection signal: {top_rejection_reasons[0]}."
|
|
734
|
+
return summary
|
|
735
|
+
|
|
736
|
+
|
|
519
737
|
def _load_fixture_corpus() -> dict[str, object]:
|
|
520
738
|
if not FIXTURE_CORPUS_PATH.exists():
|
|
521
739
|
return {"fixture_registry_version": "missing", "fixtures": []}
|
|
@@ -594,12 +812,23 @@ def _critical_capability_parity(coverage_gap: dict[str, object]) -> tuple[list[d
|
|
|
594
812
|
def _parity_counts(parity_entries: list[dict[str, object]]) -> dict[str, int]:
|
|
595
813
|
return {
|
|
596
814
|
"parity_supported_count": sum(1 for entry in parity_entries if bool(entry.get("inventory_supported"))),
|
|
815
|
+
"parity_donor_backed_ready_count": sum(1 for entry in parity_entries if bool(entry.get("donor_backed_ready"))),
|
|
597
816
|
"parity_generate_ready_count": sum(1 for entry in parity_entries if bool(entry.get("generate_supported"))),
|
|
598
817
|
"parity_acceptance_verified_count": sum(1 for entry in parity_entries if bool(entry.get("acceptance_verified"))),
|
|
599
818
|
"parity_mismatch_count": sum(1 for entry in parity_entries if entry.get("generate_mismatch_reason")),
|
|
600
819
|
}
|
|
601
820
|
|
|
602
821
|
|
|
822
|
+
def _critical_parity_counts(critical_entries: list[dict[str, object]]) -> dict[str, int]:
|
|
823
|
+
return {
|
|
824
|
+
"critical_parity_supported_count": sum(1 for entry in critical_entries if bool(entry.get("inventory_supported"))),
|
|
825
|
+
"critical_parity_donor_backed_ready_count": sum(1 for entry in critical_entries if bool(entry.get("donor_backed_ready"))),
|
|
826
|
+
"critical_parity_generate_ready_count": sum(1 for entry in critical_entries if bool(entry.get("generate_supported"))),
|
|
827
|
+
"critical_parity_acceptance_verified_count": sum(1 for entry in critical_entries if bool(entry.get("acceptance_verified"))),
|
|
828
|
+
"critical_parity_mismatch_count": sum(1 for entry in critical_entries if entry.get("generate_mismatch_reason")),
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
|
|
603
832
|
def _selected_donor_summary(
|
|
604
833
|
diagnostics: list[dict[str, object]],
|
|
605
834
|
donor_archetype: DonorArchetypePlan | None = None,
|
|
@@ -699,6 +928,10 @@ def _filter_candidates_for_blueprint(
|
|
|
699
928
|
filter_reasons.append("sample lacks donor-backed support for requested wireless mutation")
|
|
700
929
|
if "end_device_mutation" in required_capabilities and "end_device_mutation" not in supported_capabilities:
|
|
701
930
|
filter_reasons.append("sample lacks donor-backed support for requested end-device mutation")
|
|
931
|
+
if "iot_registration" in required_capabilities and "iot_registration" not in supported_capabilities:
|
|
932
|
+
filter_reasons.append("sample lacks donor-backed support for requested IoT registration")
|
|
933
|
+
if "iot_control" in required_capabilities and "iot_control" not in supported_capabilities:
|
|
934
|
+
filter_reasons.append("sample lacks donor-backed support for requested IoT control")
|
|
702
935
|
if adjusted_total_score <= 0:
|
|
703
936
|
filter_reasons.append("acceptance penalty reduced the donor score below zero")
|
|
704
937
|
if filter_reasons:
|
|
@@ -2472,8 +2705,11 @@ def _augment_coverage_gap_actions(
|
|
|
2472
2705
|
actions.append("For service-heavy prompts, prefer a donor that already contains the required server service family and core server layout.")
|
|
2473
2706
|
if scenario_family == "home_iot" and scenario_status in {"donor_limited", "acceptance_gated", "unsupported"}:
|
|
2474
2707
|
actions.append("For home IoT prompts, prefer a donor with Home Gateway plus existing IoT registration/control structure.")
|
|
2708
|
+
actions.append("For donor-backed Home IoT generate, explicitly name the thing, gateway/server target, and wireless client or SSID targets.")
|
|
2475
2709
|
if scenario_family == "wan_security_edge" and scenario_status in {"donor_limited", "acceptance_gated", "unsupported"}:
|
|
2476
2710
|
actions.append("For WAN/security prompts, prefer a donor with reusable serial/WAN or security-edge skeleton before generating.")
|
|
2711
|
+
if scenario_family in {"ipv6_routing", "l2_security_monitoring", "wireless_advanced", "automation_controller", "voice_collaboration", "industrial_iot", "physical_media_device"} and scenario_status in {"donor_limited", "acceptance_gated", "unsupported"}:
|
|
2712
|
+
actions.append("This feature family is atlas/report-first; keep it out of strict generate until donor-backed proof and acceptance fixtures exist.")
|
|
2477
2713
|
updated["recommended_next_actions"] = list(dict.fromkeys(actions))
|
|
2478
2714
|
return updated
|
|
2479
2715
|
|
|
@@ -2524,12 +2760,26 @@ def _scenario_generate_decision(
|
|
|
2524
2760
|
"service_heavy": "service-heavy",
|
|
2525
2761
|
"home_iot": "home IoT",
|
|
2526
2762
|
"wan_security_edge": "WAN/security edge",
|
|
2763
|
+
"ipv6_routing": "IPv6/routing",
|
|
2764
|
+
"l2_security_monitoring": "L2 security/monitoring",
|
|
2765
|
+
"wireless_advanced": "advanced wireless",
|
|
2766
|
+
"automation_controller": "automation/controller",
|
|
2767
|
+
"voice_collaboration": "voice/collaboration",
|
|
2768
|
+
"industrial_iot": "industrial IoT",
|
|
2769
|
+
"physical_media_device": "physical/media device",
|
|
2527
2770
|
}
|
|
2528
2771
|
expected_archetype_map = {
|
|
2529
2772
|
"campus": "campus/core",
|
|
2530
2773
|
"service_heavy": "service-heavy",
|
|
2531
2774
|
"home_iot": "IoT/home gateway",
|
|
2532
2775
|
"wan_security_edge": "WAN/security edge",
|
|
2776
|
+
"ipv6_routing": "IPv6/routing",
|
|
2777
|
+
"l2_security_monitoring": "L2 security/monitoring",
|
|
2778
|
+
"wireless_advanced": "advanced wireless",
|
|
2779
|
+
"automation_controller": "automation/controller",
|
|
2780
|
+
"voice_collaboration": "voice/collaboration",
|
|
2781
|
+
"industrial_iot": "industrial IoT",
|
|
2782
|
+
"physical_media_device": "physical/media device",
|
|
2533
2783
|
}
|
|
2534
2784
|
family_label = family_label_map.get(family, family)
|
|
2535
2785
|
blocking_reasons: list[str] = []
|
|
@@ -2543,6 +2793,13 @@ def _scenario_generate_decision(
|
|
|
2543
2793
|
expected_archetype = expected_archetype_map.get(family)
|
|
2544
2794
|
sample_archetypes = [str(item) for item in list((selected_donor_summary or {}).get("sample_archetypes", [])) if str(item).strip()]
|
|
2545
2795
|
donor_graph_summary = dict((selected_donor_summary or {}).get("donor_graph_summary") or {})
|
|
2796
|
+
best_rejected_donor_class = str((donor_selection_summary or {}).get("best_rejected_donor_class") or "").strip() or None
|
|
2797
|
+
primary_rejection_code = str((donor_selection_summary or {}).get("primary_rejection_code") or "").strip() or None
|
|
2798
|
+
best_rejected_donor_summary = _best_rejected_donor_summary(
|
|
2799
|
+
best_rejected_donor_class,
|
|
2800
|
+
primary_rejection_code,
|
|
2801
|
+
[str(item) for item in list((donor_selection_summary or {}).get("top_rejection_reasons", [])) if str(item).strip()],
|
|
2802
|
+
)
|
|
2546
2803
|
if expected_archetype and sample_archetypes:
|
|
2547
2804
|
aligned = expected_archetype in sample_archetypes
|
|
2548
2805
|
decision["selected_donor_aligned"] = aligned
|
|
@@ -2590,6 +2847,8 @@ def _scenario_generate_decision(
|
|
|
2590
2847
|
blocking_reasons.append(
|
|
2591
2848
|
f"Scenario '{family_label}' depends on donor-limited safe-open coverage, but no compatible donor was selected."
|
|
2592
2849
|
)
|
|
2850
|
+
if best_rejected_donor_summary:
|
|
2851
|
+
notes.append(best_rejected_donor_summary)
|
|
2593
2852
|
layout_status = str(donor_graph_summary.get("layout_reuse_status") or "").strip()
|
|
2594
2853
|
pair_coverage = donor_graph_summary.get("reusable_pair_coverage")
|
|
2595
2854
|
if layout_status == "weak":
|
|
@@ -2600,6 +2859,18 @@ def _scenario_generate_decision(
|
|
|
2600
2859
|
blocking_reasons.append(
|
|
2601
2860
|
f"Scenario '{family_label}' donor selection has no reusable link-pair coverage for prompt generate."
|
|
2602
2861
|
)
|
|
2862
|
+
if primary_rejection_code == "archetype_misaligned":
|
|
2863
|
+
blocking_reasons.append(
|
|
2864
|
+
f"Scenario '{family_label}' best donor class is still archetype-misaligned with the requested prompt shape."
|
|
2865
|
+
)
|
|
2866
|
+
elif primary_rejection_code == "runtime_subtree_missing":
|
|
2867
|
+
blocking_reasons.append(
|
|
2868
|
+
f"Scenario '{family_label}' donor candidates are missing required runtime subtree coverage for strict reuse."
|
|
2869
|
+
)
|
|
2870
|
+
elif primary_rejection_code == "acceptance_evidence_too_weak":
|
|
2871
|
+
blocking_reasons.append(
|
|
2872
|
+
f"Scenario '{family_label}' donor candidates still lack strong enough acceptance evidence for strict prompt generate."
|
|
2873
|
+
)
|
|
2603
2874
|
if layout_status in {"strong", "partial"} and isinstance(pair_coverage, int) and pair_coverage > 0:
|
|
2604
2875
|
notes.append(f"selected donor provides {pair_coverage}% reusable link-pair coverage ({layout_status})")
|
|
2605
2876
|
if selected_donor_summary and not blocking_reasons:
|
|
@@ -2668,12 +2939,6 @@ def _scenario_acceptance_summary(
|
|
|
2668
2939
|
donor_state = "candidate_pool_blocked"
|
|
2669
2940
|
if donor_state == "not_selected" and int(candidate_counts.get("selected", 0) or 0) > 0:
|
|
2670
2941
|
donor_state = "selection_pending"
|
|
2671
|
-
selection_failure_type = None
|
|
2672
|
-
if not selected_donor_summary:
|
|
2673
|
-
if donor_state in {"candidate_pool_blocked", "selection_pending"}:
|
|
2674
|
-
selection_failure_type = "viable_donor_found_but_acceptance_weak"
|
|
2675
|
-
else:
|
|
2676
|
-
selection_failure_type = "no_viable_donor_found"
|
|
2677
2942
|
key_reasons = [str(item) for item in decision.get("blocking_reasons", []) if str(item).strip()]
|
|
2678
2943
|
if not key_reasons:
|
|
2679
2944
|
key_reasons = [str(item) for item in decision.get("notes", []) if str(item).strip()]
|
|
@@ -2682,11 +2947,19 @@ def _scenario_acceptance_summary(
|
|
|
2682
2947
|
None,
|
|
2683
2948
|
)
|
|
2684
2949
|
top_rejection_reasons = [str(item) for item in list((donor_selection_summary or {}).get("top_rejection_reasons", [])) if str(item).strip()]
|
|
2950
|
+
primary_rejection_code = str((donor_selection_summary or {}).get("primary_rejection_code") or "").strip() or _primary_rejection_code(top_rejection_reasons)
|
|
2951
|
+
primary_rejection_layer = str((donor_selection_summary or {}).get("primary_rejection_layer") or "").strip() or _primary_rejection_layer(primary_rejection_code)
|
|
2952
|
+
best_rejected_donor_class = str((donor_selection_summary or {}).get("best_rejected_donor_class") or "").strip() or None
|
|
2953
|
+
selection_failure_type = None
|
|
2685
2954
|
if not selected_donor_summary:
|
|
2686
|
-
if
|
|
2955
|
+
if primary_rejection_code == "archetype_misaligned":
|
|
2687
2956
|
selection_failure_type = "viable_donor_found_but_archetype_misaligned"
|
|
2688
|
-
elif
|
|
2957
|
+
elif primary_rejection_code == "runtime_subtree_missing":
|
|
2689
2958
|
selection_failure_type = "viable_donor_found_but_runtime_subtree_missing"
|
|
2959
|
+
elif donor_state in {"candidate_pool_blocked", "selection_pending"}:
|
|
2960
|
+
selection_failure_type = "viable_donor_found_but_acceptance_weak"
|
|
2961
|
+
else:
|
|
2962
|
+
selection_failure_type = "no_viable_donor_found"
|
|
2690
2963
|
critical_capability_parity, critical_parity_mismatches = _critical_capability_parity(coverage_gap)
|
|
2691
2964
|
best_available_donor_class = next(
|
|
2692
2965
|
(
|
|
@@ -2696,11 +2969,39 @@ def _scenario_acceptance_summary(
|
|
|
2696
2969
|
),
|
|
2697
2970
|
None,
|
|
2698
2971
|
)
|
|
2972
|
+
if best_available_donor_class is None:
|
|
2973
|
+
family_donor_class_map = {
|
|
2974
|
+
"campus": "campus/core",
|
|
2975
|
+
"service_heavy": "service-heavy",
|
|
2976
|
+
"home_iot": "IoT/home gateway",
|
|
2977
|
+
"wan_security_edge": "WAN/security edge",
|
|
2978
|
+
"ipv6_routing": "IPv6/routing",
|
|
2979
|
+
"l2_security_monitoring": "L2 security/monitoring",
|
|
2980
|
+
"wireless_advanced": "advanced wireless",
|
|
2981
|
+
"automation_controller": "automation/controller",
|
|
2982
|
+
"voice_collaboration": "voice/collaboration",
|
|
2983
|
+
"industrial_iot": "industrial IoT",
|
|
2984
|
+
"physical_media_device": "physical/media device",
|
|
2985
|
+
}
|
|
2986
|
+
best_available_donor_class = family_donor_class_map.get(str(readiness.get("family") or "").strip())
|
|
2987
|
+
if selected_donor_summary:
|
|
2988
|
+
best_rejected_donor_class = None
|
|
2989
|
+
primary_rejection_code = None
|
|
2990
|
+
primary_rejection_layer = None
|
|
2991
|
+
elif best_rejected_donor_class is None:
|
|
2992
|
+
best_rejected_donor_class = best_available_donor_class
|
|
2699
2993
|
remediation_steps = [
|
|
2700
2994
|
str(item)
|
|
2701
2995
|
for item in list(coverage_gap.get("recommended_next_actions", []))
|
|
2702
2996
|
if str(item).strip()
|
|
2703
2997
|
][:3]
|
|
2998
|
+
best_rejected_donor_summary = _best_rejected_donor_summary(
|
|
2999
|
+
best_rejected_donor_class,
|
|
3000
|
+
primary_rejection_code,
|
|
3001
|
+
top_rejection_reasons,
|
|
3002
|
+
)
|
|
3003
|
+
if best_rejected_donor_summary:
|
|
3004
|
+
remediation_steps = [best_rejected_donor_summary, *remediation_steps][:3]
|
|
2704
3005
|
missing_donor_classes = [best_available_donor_class] if selection_failure_type == "viable_donor_found_but_archetype_misaligned" and best_available_donor_class else []
|
|
2705
3006
|
critical_runtime_requirements: list[str] = []
|
|
2706
3007
|
if runtime_blocked:
|
|
@@ -2721,14 +3022,19 @@ def _scenario_acceptance_summary(
|
|
|
2721
3022
|
"missing_critical_capabilities": [str(item) for item in list(readiness.get("missing_critical_capabilities", [])) if str(item).strip()],
|
|
2722
3023
|
"critical_capability_parity": critical_capability_parity,
|
|
2723
3024
|
"critical_parity_mismatches": critical_parity_mismatches,
|
|
3025
|
+
**_critical_parity_counts(critical_capability_parity),
|
|
2724
3026
|
"key_reasons": key_reasons[:3],
|
|
2725
3027
|
"next_best_action": next_best_action,
|
|
2726
3028
|
"remediation_steps": remediation_steps,
|
|
2727
3029
|
"best_available_donor_class": best_available_donor_class,
|
|
3030
|
+
"best_rejected_donor_class": best_rejected_donor_class,
|
|
3031
|
+
"best_rejected_donor_summary": best_rejected_donor_summary,
|
|
2728
3032
|
"missing_donor_classes": missing_donor_classes,
|
|
2729
3033
|
"critical_runtime_requirements": critical_runtime_requirements,
|
|
2730
3034
|
"decision_confidence": decision.get("decision_confidence"),
|
|
2731
3035
|
"blocking_layer": decision.get("blocking_layer"),
|
|
3036
|
+
"primary_rejection_layer": primary_rejection_layer,
|
|
3037
|
+
"primary_rejection_code": primary_rejection_code,
|
|
2732
3038
|
}
|
|
2733
3039
|
|
|
2734
3040
|
|
|
@@ -2808,6 +3114,7 @@ def _scenario_matrix_row(
|
|
|
2808
3114
|
"decision_confidence": summary.get("decision_confidence"),
|
|
2809
3115
|
"blocking_layer": summary.get("blocking_layer"),
|
|
2810
3116
|
"best_available_donor_class": summary.get("best_available_donor_class"),
|
|
3117
|
+
"primary_rejection_code": summary.get("primary_rejection_code"),
|
|
2811
3118
|
"remediation_hint": next((str(item) for item in list(summary.get("remediation_steps", []) or []) if str(item).strip()), None),
|
|
2812
3119
|
}
|
|
2813
3120
|
|
|
@@ -2944,6 +3251,8 @@ def generate_from_prompt(
|
|
|
2944
3251
|
remote_provider: str = "github",
|
|
2945
3252
|
import_cache_root: Path | None = None,
|
|
2946
3253
|
max_remote_results: int = 10,
|
|
3254
|
+
remote_dry_run: bool = False,
|
|
3255
|
+
remote_audit_out: Path | None = None,
|
|
2947
3256
|
blueprint_out_path: Path | None = None,
|
|
2948
3257
|
) -> None:
|
|
2949
3258
|
raw_plan = parse_intent(prompt)
|
|
@@ -2955,6 +3264,8 @@ def generate_from_prompt(
|
|
|
2955
3264
|
remote_provider=remote_provider,
|
|
2956
3265
|
import_cache_root=import_cache_root,
|
|
2957
3266
|
max_remote_results=max_remote_results,
|
|
3267
|
+
remote_dry_run=remote_dry_run,
|
|
3268
|
+
remote_audit_out=remote_audit_out,
|
|
2958
3269
|
)
|
|
2959
3270
|
raw_plan.remote_search_results = remote_results
|
|
2960
3271
|
if raw_plan.goal == "edit" and raw_plan.pkt_path:
|
|
@@ -2984,7 +3295,7 @@ def generate_from_prompt(
|
|
|
2984
3295
|
)
|
|
2985
3296
|
coverage_gap = _augment_coverage_gap_actions(
|
|
2986
3297
|
coverage_gap,
|
|
2987
|
-
donor_blocking_reason=
|
|
3298
|
+
donor_blocking_reason=_inspect_packet_tracer_compatibility_donor_cached().blocking_reason,
|
|
2988
3299
|
)
|
|
2989
3300
|
scenario_generate_decision = _scenario_generate_decision(coverage_gap)
|
|
2990
3301
|
prepared_plan.remote_search_results = remote_results
|
|
@@ -3071,6 +3382,8 @@ def _explain_plan_payload(
|
|
|
3071
3382
|
remote_provider: str = "github",
|
|
3072
3383
|
import_cache_root: Path | None = None,
|
|
3073
3384
|
max_remote_results: int = 10,
|
|
3385
|
+
remote_dry_run: bool = False,
|
|
3386
|
+
remote_audit_out: Path | None = None,
|
|
3074
3387
|
) -> dict[str, object]:
|
|
3075
3388
|
raw_plan = parse_intent(prompt)
|
|
3076
3389
|
resolved_reference_roots, resolved_donor_roots, remote_results = _resolve_remote_sources(
|
|
@@ -3081,10 +3394,12 @@ def _explain_plan_payload(
|
|
|
3081
3394
|
remote_provider=remote_provider,
|
|
3082
3395
|
import_cache_root=import_cache_root,
|
|
3083
3396
|
max_remote_results=max_remote_results,
|
|
3397
|
+
remote_dry_run=remote_dry_run,
|
|
3398
|
+
remote_audit_out=remote_audit_out,
|
|
3084
3399
|
)
|
|
3085
3400
|
plan = _apply_prompt_compatibility_requirements(raw_plan, resolved_donor_roots)
|
|
3086
3401
|
plan.remote_search_results = remote_results
|
|
3087
|
-
donor_details =
|
|
3402
|
+
donor_details = _inspect_packet_tracer_compatibility_donor_cached()
|
|
3088
3403
|
compat_donor, compat_donor_version = donor_details.resolved_path, donor_details.donor_version
|
|
3089
3404
|
topology_tags = _topology_tags_for_plan(plan, _choose_topology_archetype(plan))
|
|
3090
3405
|
result: dict[str, object] = {
|
|
@@ -3213,8 +3528,22 @@ def _explain_plan_payload(
|
|
|
3213
3528
|
try:
|
|
3214
3529
|
if evaluation is None:
|
|
3215
3530
|
raise PlanningError("Prompt plan is incomplete; generation was skipped.", prepared)
|
|
3216
|
-
adapted_plan, donor_archetype, donor_root,
|
|
3531
|
+
adapted_plan, donor_archetype, donor_root, selected_candidate = evaluation
|
|
3217
3532
|
safe_plan, profiled_plan = _apply_safe_open_profile(donor_root, adapted_plan)
|
|
3533
|
+
matrix_hits, coverage_gap, blueprint_plan = _build_support_reports(
|
|
3534
|
+
profiled_plan,
|
|
3535
|
+
blueprint=blueprint,
|
|
3536
|
+
cisco_ranked=cisco_ranked,
|
|
3537
|
+
curated_ranked=curated_ranked,
|
|
3538
|
+
reference_catalog=reference_catalog,
|
|
3539
|
+
selected_donor=selected_candidate.sample.relative_path,
|
|
3540
|
+
)
|
|
3541
|
+
coverage_gap = _augment_coverage_gap_actions(
|
|
3542
|
+
coverage_gap,
|
|
3543
|
+
donor_diagnostics=diagnostics,
|
|
3544
|
+
donor_selection_summary=result["donor_selection_summary"],
|
|
3545
|
+
donor_blocking_reason=donor_details.blocking_reason,
|
|
3546
|
+
)
|
|
3218
3547
|
profiled_plan.remote_search_results = remote_results
|
|
3219
3548
|
profiled_plan.capability_matrix_hits = matrix_hits
|
|
3220
3549
|
profiled_plan.coverage_gap_report = coverage_gap
|
|
@@ -3269,6 +3598,11 @@ def _explain_plan_payload(
|
|
|
3269
3598
|
result["scenario_acceptance_summary"],
|
|
3270
3599
|
selected_donor_summary=result["selected_donor_summary"],
|
|
3271
3600
|
)
|
|
3601
|
+
result["capability_matrix_hits"] = matrix_hits
|
|
3602
|
+
result["unsupported_capabilities"] = coverage_gap.get("unsupported_capabilities", [])
|
|
3603
|
+
result["coverage_gaps"] = coverage_gap
|
|
3604
|
+
result["capability_parity"] = list(coverage_gap.get("capability_parity", []))
|
|
3605
|
+
result["blueprint_plan"] = blueprint_plan
|
|
3272
3606
|
except PlanningError as exc:
|
|
3273
3607
|
if result["donor_rejection_reasons"]:
|
|
3274
3608
|
for item in result["donor_rejection_reasons"]:
|
|
@@ -3336,6 +3670,8 @@ def _explain_plan_payload(
|
|
|
3336
3670
|
result["scenario_matrix_row"]["fixture_name"] = _scenario_fixture_name(result["scenario_matrix_row"].get("family"))
|
|
3337
3671
|
result["scenario_matrix_row"]["matrix_version"] = "2.1"
|
|
3338
3672
|
result["scenario_matrix_row"].update(_parity_counts(_capability_parity_entries(result.get("coverage_gaps", {}))))
|
|
3673
|
+
critical_capability_parity, _critical_parity_mismatches = _critical_capability_parity(dict(result.get("coverage_gaps") or {}))
|
|
3674
|
+
result["scenario_matrix_row"].update(_critical_parity_counts(critical_capability_parity))
|
|
3339
3675
|
fixture_status, fixture_gaps = _fixture_expectation_status(result)
|
|
3340
3676
|
result["fixture_registry_version"] = _load_fixture_corpus().get("fixture_registry_version", "unknown")
|
|
3341
3677
|
result["fixture_expectation_status"] = fixture_status
|
|
@@ -3352,6 +3688,8 @@ def explain_plan(
|
|
|
3352
3688
|
remote_provider: str = "github",
|
|
3353
3689
|
import_cache_root: Path | None = None,
|
|
3354
3690
|
max_remote_results: int = 10,
|
|
3691
|
+
remote_dry_run: bool = False,
|
|
3692
|
+
remote_audit_out: Path | None = None,
|
|
3355
3693
|
acceptance_json_out: Path | None = None,
|
|
3356
3694
|
) -> None:
|
|
3357
3695
|
result = _explain_plan_payload(
|
|
@@ -3362,6 +3700,8 @@ def explain_plan(
|
|
|
3362
3700
|
remote_provider=remote_provider,
|
|
3363
3701
|
import_cache_root=import_cache_root,
|
|
3364
3702
|
max_remote_results=max_remote_results,
|
|
3703
|
+
remote_dry_run=remote_dry_run,
|
|
3704
|
+
remote_audit_out=remote_audit_out,
|
|
3365
3705
|
)
|
|
3366
3706
|
_write_json_artifact(result, acceptance_json_out)
|
|
3367
3707
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
@@ -3377,7 +3717,7 @@ def inventory_pkt(
|
|
|
3377
3717
|
root = ET.fromstring(decode_pkt_modern(pkt_path.read_bytes()))
|
|
3378
3718
|
payload = inventory_root(root)
|
|
3379
3719
|
workspace = inspect_workspace_integrity(root)
|
|
3380
|
-
donor_details =
|
|
3720
|
+
donor_details = _inspect_packet_tracer_compatibility_donor_cached()
|
|
3381
3721
|
compat_donor, compat_donor_version = donor_details.resolved_path, donor_details.donor_version
|
|
3382
3722
|
payload["workspace_validation"] = {
|
|
3383
3723
|
"workspace_mode": workspace.workspace_mode,
|
|
@@ -3552,6 +3892,8 @@ def parity_report(
|
|
|
3552
3892
|
remote_provider: str = "github",
|
|
3553
3893
|
import_cache_root: Path | None = None,
|
|
3554
3894
|
max_remote_results: int = 10,
|
|
3895
|
+
remote_dry_run: bool = False,
|
|
3896
|
+
remote_audit_out: Path | None = None,
|
|
3555
3897
|
acceptance_json_out: Path | None = None,
|
|
3556
3898
|
) -> None:
|
|
3557
3899
|
payload = _explain_plan_payload(
|
|
@@ -3562,6 +3904,8 @@ def parity_report(
|
|
|
3562
3904
|
remote_provider=remote_provider,
|
|
3563
3905
|
import_cache_root=import_cache_root,
|
|
3564
3906
|
max_remote_results=max_remote_results,
|
|
3907
|
+
remote_dry_run=remote_dry_run,
|
|
3908
|
+
remote_audit_out=remote_audit_out,
|
|
3565
3909
|
)
|
|
3566
3910
|
coverage_gap = dict(payload.get("coverage_gaps") or {})
|
|
3567
3911
|
if "capability_parity" not in coverage_gap:
|
|
@@ -3574,6 +3918,7 @@ def parity_report(
|
|
|
3574
3918
|
"critical_capability_parity": critical_capability_parity,
|
|
3575
3919
|
"critical_parity_mismatches": critical_parity_mismatches,
|
|
3576
3920
|
**_parity_counts(list(payload.get("capability_parity", []))),
|
|
3921
|
+
**_critical_parity_counts(critical_capability_parity),
|
|
3577
3922
|
}
|
|
3578
3923
|
_write_json_artifact(result, acceptance_json_out)
|
|
3579
3924
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
@@ -3588,6 +3933,8 @@ def compare_scenarios(
|
|
|
3588
3933
|
remote_provider: str = "github",
|
|
3589
3934
|
import_cache_root: Path | None = None,
|
|
3590
3935
|
max_remote_results: int = 10,
|
|
3936
|
+
remote_dry_run: bool = False,
|
|
3937
|
+
remote_audit_out: Path | None = None,
|
|
3591
3938
|
matrix_out: Path | None = None,
|
|
3592
3939
|
acceptance_json_out: Path | None = None,
|
|
3593
3940
|
) -> None:
|
|
@@ -3602,6 +3949,8 @@ def compare_scenarios(
|
|
|
3602
3949
|
remote_provider=remote_provider,
|
|
3603
3950
|
import_cache_root=import_cache_root,
|
|
3604
3951
|
max_remote_results=max_remote_results,
|
|
3952
|
+
remote_dry_run=remote_dry_run,
|
|
3953
|
+
remote_audit_out=remote_audit_out,
|
|
3605
3954
|
)
|
|
3606
3955
|
fixture_status, fixture_gaps = _fixture_expectation_status(payload)
|
|
3607
3956
|
payloads.append(
|
|
@@ -3635,6 +3984,94 @@ def compare_scenarios(
|
|
|
3635
3984
|
print(rendered)
|
|
3636
3985
|
|
|
3637
3986
|
|
|
3987
|
+
def feature_gap_report() -> None:
|
|
3988
|
+
print(json.dumps(build_feature_gap_report(), indent=2, ensure_ascii=False))
|
|
3989
|
+
|
|
3990
|
+
|
|
3991
|
+
LOCAL_AUDIT_PATTERNS = {
|
|
3992
|
+
"ospfv2": r"(?mi)^\s*router\s+ospf\s+\d+",
|
|
3993
|
+
"eigrp_ipv4": r"(?mi)^\s*router\s+eigrp\s+\d+",
|
|
3994
|
+
"ripv2": r"(?mi)^\s*router\s+rip\s*$",
|
|
3995
|
+
"static_route": r"(?mi)^\s*ip\s+route\s+\S+\s+\S+\s+\S+",
|
|
3996
|
+
"default_route": r"(?mi)^\s*ip\s+route\s+0\.0\.0\.0\s+0\.0\.0\.0\s+\S+",
|
|
3997
|
+
"dhcp_relay": r"(?mi)^\s*ip\s+helper-address\s+\d+\.\d+\.\d+\.\d+",
|
|
3998
|
+
"nat_static": r"(?mi)^\s*ip\s+nat\s+inside\s+source\s+static\b",
|
|
3999
|
+
"nat_dynamic": r"(?mi)^\s*ip\s+nat\s+inside\s+source\s+(?:list|route-map)\b",
|
|
4000
|
+
"pat": r"(?mi)^\s*ip\s+nat\s+inside\s+source\s+list\s+\S+\s+interface\s+\S+\s+overload\b",
|
|
4001
|
+
"ssh_ios": r"(?mi)^\s*(?:ip\s+ssh|crypto\s+key\s+generate|ip\s+domain-name)\b",
|
|
4002
|
+
"ntp_ios": r"(?mi)^\s*ntp\s+server\s+\d+\.\d+\.\d+\.\d+",
|
|
4003
|
+
"syslog_ios": r"(?mi)^\s*logging\s+host\s+\d+\.\d+\.\d+\.\d+",
|
|
4004
|
+
"bgp": r"(?mi)^\s*router\s+bgp\s+\d+",
|
|
4005
|
+
"stp": r"(?mi)^\s*spanning-tree\b",
|
|
4006
|
+
"etherchannel": r"(?mi)^\s*(?:channel-group\s+\d+|interface\s+Port-channel)",
|
|
4007
|
+
"hsrp": r"(?mi)^\s*standby\s+\d+",
|
|
4008
|
+
"acl": r"(?mi)^\s*(?:access-list\s+\d+|ip\s+access-list\s+(?:standard|extended))\b",
|
|
4009
|
+
"dhcp_pool": r"(?mi)^\s*ip\s+dhcp\s+pool\b",
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
|
|
4013
|
+
def build_local_sample_audit(root: Path) -> dict[str, object]:
|
|
4014
|
+
files = sorted([*root.rglob("*.pkt"), *root.rglob("*.pka")])
|
|
4015
|
+
capability_counts: Counter[str] = Counter()
|
|
4016
|
+
capability_examples: dict[str, list[str]] = defaultdict(list)
|
|
4017
|
+
device_counts: Counter[str] = Counter()
|
|
4018
|
+
decode_failures: list[dict[str, str]] = []
|
|
4019
|
+
for pkt_path in files:
|
|
4020
|
+
rel = str(pkt_path.relative_to(root))
|
|
4021
|
+
try:
|
|
4022
|
+
pkt_root = decode_pkt_to_root(pkt_path)
|
|
4023
|
+
except Exception as exc:
|
|
4024
|
+
decode_failures.append({"path": rel, "error_type": type(exc).__name__, "message": str(exc)[:200]})
|
|
4025
|
+
continue
|
|
4026
|
+
try:
|
|
4027
|
+
inventory = inventory_root(pkt_root)
|
|
4028
|
+
for device_type, count in inventory.get("topology_summary", {}).get("device_counts", {}).items():
|
|
4029
|
+
device_counts[str(device_type)] += int(count)
|
|
4030
|
+
except Exception:
|
|
4031
|
+
pass
|
|
4032
|
+
running = "\n".join(line.text or "" for line in pkt_root.findall(".//ENGINE/RUNNINGCONFIG/LINE"))
|
|
4033
|
+
for capability, pattern in LOCAL_AUDIT_PATTERNS.items():
|
|
4034
|
+
if re.search(pattern, running):
|
|
4035
|
+
capability_counts[capability] += 1
|
|
4036
|
+
if len(capability_examples[capability]) < 5:
|
|
4037
|
+
capability_examples[capability].append(rel)
|
|
4038
|
+
promotion_candidates = [
|
|
4039
|
+
{
|
|
4040
|
+
"capability": capability,
|
|
4041
|
+
"sample_count": count,
|
|
4042
|
+
"candidate_status": "local_evidence_only",
|
|
4043
|
+
"next_safe_action": "Add parser/inventory/editor roundtrip proof before public readiness promotion.",
|
|
4044
|
+
}
|
|
4045
|
+
for capability, count in capability_counts.most_common()
|
|
4046
|
+
]
|
|
4047
|
+
return {
|
|
4048
|
+
"audit_version": "1.0",
|
|
4049
|
+
"source_root": str(root),
|
|
4050
|
+
"policy": "Local user-supplied Packet Tracer files are evidence inputs only; raw .pkt/.pka files are not public package content.",
|
|
4051
|
+
"total_files": len(files),
|
|
4052
|
+
"decode_success_count": len(files) - len(decode_failures),
|
|
4053
|
+
"decode_fail_count": len(decode_failures),
|
|
4054
|
+
"decode_failures": decode_failures[:20],
|
|
4055
|
+
"detected_config_capabilities": {
|
|
4056
|
+
capability: {
|
|
4057
|
+
"sample_count": count,
|
|
4058
|
+
"examples": capability_examples.get(capability, []),
|
|
4059
|
+
}
|
|
4060
|
+
for capability, count in capability_counts.most_common()
|
|
4061
|
+
},
|
|
4062
|
+
"top_device_types": [{"device_type": device_type, "count": count} for device_type, count in device_counts.most_common(30)],
|
|
4063
|
+
"promotion_candidates": promotion_candidates,
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
|
|
4067
|
+
def local_sample_audit(root: Path, audit_out: Path | None = None) -> None:
|
|
4068
|
+
payload = build_local_sample_audit(root)
|
|
4069
|
+
target = audit_out or (Path("output") / "local-sample-audit.json")
|
|
4070
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
4071
|
+
target.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
4072
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
4073
|
+
|
|
4074
|
+
|
|
3638
4075
|
def main() -> None:
|
|
3639
4076
|
parser = argparse.ArgumentParser(description="Generate or inspect Cisco Packet Tracer 9.0 .pkt files")
|
|
3640
4077
|
parser.add_argument("--blueprint", help="Path to the topology blueprint JSON")
|
|
@@ -3656,8 +4093,13 @@ def main() -> None:
|
|
|
3656
4093
|
parser.add_argument("--remote-provider", default="github", help="Remote search provider name (default: github)")
|
|
3657
4094
|
parser.add_argument("--import-cache-root", help="Local cache folder used for remote search auto-imports")
|
|
3658
4095
|
parser.add_argument("--max-remote-results", type=int, default=10, help="Maximum number of remote search results to fetch/import")
|
|
4096
|
+
parser.add_argument("--remote-dry-run", action="store_true", help="Preview remote GitHub sample candidates and write an audit without downloading archives")
|
|
4097
|
+
parser.add_argument("--remote-audit-out", help="Optional JSON path for the local-only remote sample audit report")
|
|
3659
4098
|
parser.add_argument("--blueprint-out", help="Optional JSON output path for the generated blueprint plan or refusal blueprint")
|
|
3660
4099
|
parser.add_argument("--coverage-report", action="store_true", help="Print the aggregated capability coverage matrix")
|
|
4100
|
+
parser.add_argument("--feature-gap-report", action="store_true", help="Print the Packet Tracer feature gap atlas report")
|
|
4101
|
+
parser.add_argument("--local-sample-audit-root", help="Local user-supplied Packet Tracer sample folder to audit without importing raw files")
|
|
4102
|
+
parser.add_argument("--local-sample-audit-out", help="Optional JSON path for --local-sample-audit-root output")
|
|
3661
4103
|
parser.add_argument("--inventory-capabilities", action="store_true", help="Include inferred capability inventory when using --inventory")
|
|
3662
4104
|
parser.add_argument("--inventory-out", help="Optional JSON output path when using --inventory")
|
|
3663
4105
|
parser.add_argument("--matrix-out", help="Optional JSON output path when using --compare-scenarios")
|
|
@@ -3669,6 +4111,7 @@ def main() -> None:
|
|
|
3669
4111
|
reference_roots = [Path(path) for path in (args.reference_root or [])]
|
|
3670
4112
|
donor_roots = [Path(path) for path in (args.donor_root or [])]
|
|
3671
4113
|
import_cache_root = Path(args.import_cache_root) if args.import_cache_root else None
|
|
4114
|
+
remote_audit_out = Path(args.remote_audit_out) if args.remote_audit_out else None
|
|
3672
4115
|
|
|
3673
4116
|
if args.explain_plan:
|
|
3674
4117
|
explain_plan(
|
|
@@ -3679,6 +4122,8 @@ def main() -> None:
|
|
|
3679
4122
|
remote_provider=args.remote_provider,
|
|
3680
4123
|
import_cache_root=import_cache_root,
|
|
3681
4124
|
max_remote_results=args.max_remote_results,
|
|
4125
|
+
remote_dry_run=args.remote_dry_run,
|
|
4126
|
+
remote_audit_out=remote_audit_out,
|
|
3682
4127
|
acceptance_json_out=Path(args.acceptance_json_out) if args.acceptance_json_out else None,
|
|
3683
4128
|
)
|
|
3684
4129
|
return
|
|
@@ -3691,6 +4136,8 @@ def main() -> None:
|
|
|
3691
4136
|
remote_provider=args.remote_provider,
|
|
3692
4137
|
import_cache_root=import_cache_root,
|
|
3693
4138
|
max_remote_results=args.max_remote_results,
|
|
4139
|
+
remote_dry_run=args.remote_dry_run,
|
|
4140
|
+
remote_audit_out=remote_audit_out,
|
|
3694
4141
|
matrix_out=Path(args.matrix_out) if args.matrix_out else None,
|
|
3695
4142
|
acceptance_json_out=Path(args.acceptance_json_out) if args.acceptance_json_out else None,
|
|
3696
4143
|
)
|
|
@@ -3704,6 +4151,8 @@ def main() -> None:
|
|
|
3704
4151
|
remote_provider=args.remote_provider,
|
|
3705
4152
|
import_cache_root=import_cache_root,
|
|
3706
4153
|
max_remote_results=args.max_remote_results,
|
|
4154
|
+
remote_dry_run=args.remote_dry_run,
|
|
4155
|
+
remote_audit_out=remote_audit_out,
|
|
3707
4156
|
acceptance_json_out=Path(args.acceptance_json_out) if args.acceptance_json_out else None,
|
|
3708
4157
|
)
|
|
3709
4158
|
return
|
|
@@ -3725,6 +4174,12 @@ def main() -> None:
|
|
|
3725
4174
|
if args.coverage_report:
|
|
3726
4175
|
coverage_report(reference_roots, donor_roots, device_family=args.device_family)
|
|
3727
4176
|
return
|
|
4177
|
+
if args.feature_gap_report:
|
|
4178
|
+
feature_gap_report()
|
|
4179
|
+
return
|
|
4180
|
+
if args.local_sample_audit_root:
|
|
4181
|
+
local_sample_audit(Path(args.local_sample_audit_root), Path(args.local_sample_audit_out) if args.local_sample_audit_out else None)
|
|
4182
|
+
return
|
|
3728
4183
|
if args.decode:
|
|
3729
4184
|
if not args.xml_out:
|
|
3730
4185
|
parser.error("--decode requires --xml-out")
|
|
@@ -3756,6 +4211,8 @@ def main() -> None:
|
|
|
3756
4211
|
remote_provider=args.remote_provider,
|
|
3757
4212
|
import_cache_root=import_cache_root,
|
|
3758
4213
|
max_remote_results=args.max_remote_results,
|
|
4214
|
+
remote_dry_run=args.remote_dry_run,
|
|
4215
|
+
remote_audit_out=remote_audit_out,
|
|
3759
4216
|
blueprint_out_path=Path(args.blueprint_out) if args.blueprint_out else None,
|
|
3760
4217
|
)
|
|
3761
4218
|
except PlanningError as exc:
|