packet-tracer-skill 0.2.0 → 0.2.2
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 +42 -4
- package/README.md +237 -88
- package/docs/campus-donor-proof.md +103 -0
- package/docs/curated-donor-registry.md +26 -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/hero-demo-plan.md +18 -10
- package/docs/home-iot-donor-proof.md +64 -0
- package/docs/launch-announcement-0.2.1.md +15 -0
- package/docs/launch-announcement-0.2.2.md +15 -0
- package/docs/packet-tracer-feature-gap-atlas.md +87 -0
- package/docs/post-launch-follow-up.md +39 -0
- package/docs/publish-preview-roadmap.md +20 -17
- package/docs/release-checklist.md +22 -12
- package/docs/{release-notes-0.2.0.md → release-notes-0.2.1.md} +7 -6
- package/docs/release-notes-0.2.2.md +26 -0
- package/docs/runtime-truth.md +31 -0
- package/docs/wan-security-donor-proof.md +41 -0
- package/docs/wireless-advanced-proof.md +60 -0
- package/examples/README.md +13 -10
- package/examples/gallery.md +10 -4
- package/examples/index.json +3 -3
- package/package.json +44 -16
- package/references/curated-donor-registry.json +5 -2
- package/references/packettracer-feature-atlas.json +133 -0
- package/references/scenario-fixture-corpus.json +1 -0
- package/scripts/build_examples_index.py +27 -16
- package/scripts/coverage_matrix.py +559 -23
- package/scripts/feature_atlas.py +229 -0
- package/scripts/generate_pkt.py +330 -31
- package/scripts/intent_parser.py +278 -7
- package/scripts/pkt_editor.py +174 -0
- package/scripts/runtime_doctor.py +83 -10
- package/scripts/sample_catalog.py +210 -5
- package/docs/screenshots/.gitkeep +0 -1
- package/docs/screenshots/packet-tracer-topology-cropped.png +0 -0
- package/scripts/__pycache__/build_examples_index.cpython-314.pyc +0 -0
- package/scripts/__pycache__/build_sample_catalog.cpython-314.pyc +0 -0
- package/scripts/__pycache__/coverage_matrix.cpython-314.pyc +0 -0
- package/scripts/__pycache__/donor_diagnostics.cpython-314.pyc +0 -0
- package/scripts/__pycache__/generate_pkt.cpython-314.pyc +0 -0
- package/scripts/__pycache__/install_skill.cpython-314.pyc +0 -0
- package/scripts/__pycache__/intent_parser.cpython-314.pyc +0 -0
- package/scripts/__pycache__/packet_tracer_env.cpython-314.pyc +0 -0
- package/scripts/__pycache__/pkt_builder.cpython-314.pyc +0 -0
- package/scripts/__pycache__/pkt_codec.cpython-314.pyc +0 -0
- package/scripts/__pycache__/pkt_editor.cpython-314.pyc +0 -0
- package/scripts/__pycache__/pkt_transformer.cpython-314.pyc +0 -0
- package/scripts/__pycache__/remote_search.cpython-314.pyc +0 -0
- package/scripts/__pycache__/runtime_doctor.cpython-314.pyc +0 -0
- package/scripts/__pycache__/sample_catalog.cpython-314.pyc +0 -0
- package/scripts/__pycache__/sample_selector.cpython-314.pyc +0 -0
- package/scripts/__pycache__/twofish_diagnostics.cpython-314.pyc +0 -0
- package/scripts/__pycache__/twofish_runtime.cpython-314.pyc +0 -0
- package/scripts/__pycache__/workspace_repair.cpython-314.pyc +0 -0
- package/scripts/vendor/__pycache__/twofish.cpython-314.pyc +0 -0
package/scripts/generate_pkt.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import argparse
|
|
6
6
|
import copy
|
|
7
7
|
from dataclasses import asdict, dataclass, field
|
|
8
|
+
from functools import lru_cache
|
|
8
9
|
import json
|
|
9
10
|
import os
|
|
10
11
|
from pathlib import Path
|
|
@@ -25,6 +26,7 @@ from coverage_matrix import (
|
|
|
25
26
|
build_inventory_capability_report,
|
|
26
27
|
select_capability_matrix_hits,
|
|
27
28
|
)
|
|
29
|
+
from feature_atlas import build_feature_gap_report
|
|
28
30
|
from intent_parser import IntentPlan, parse_intent
|
|
29
31
|
from packet_tracer_env import (
|
|
30
32
|
get_packet_tracer_compatibility_donor,
|
|
@@ -107,6 +109,11 @@ if hasattr(sys.stdout, "reconfigure"):
|
|
|
107
109
|
pass
|
|
108
110
|
|
|
109
111
|
|
|
112
|
+
@lru_cache(maxsize=1)
|
|
113
|
+
def _inspect_packet_tracer_compatibility_donor_cached():
|
|
114
|
+
return inspect_packet_tracer_compatibility_donor()
|
|
115
|
+
|
|
116
|
+
|
|
110
117
|
class PlanningError(RuntimeError):
|
|
111
118
|
def __init__(self, message: str, plan: IntentPlan) -> None:
|
|
112
119
|
super().__init__(message)
|
|
@@ -139,7 +146,7 @@ STRICT_COMPATIBILITY_GAP = (
|
|
|
139
146
|
|
|
140
147
|
|
|
141
148
|
def _compat_donor_details() -> tuple[Path | None, str | None]:
|
|
142
|
-
details =
|
|
149
|
+
details = _inspect_packet_tracer_compatibility_donor_cached()
|
|
143
150
|
return details.resolved_path, details.donor_version
|
|
144
151
|
|
|
145
152
|
|
|
@@ -327,28 +334,97 @@ def _preferred_donor_archetypes_for_plan(plan: IntentPlan, topology_tags: list[s
|
|
|
327
334
|
}
|
|
328
335
|
capabilities = set(plan.capabilities)
|
|
329
336
|
tags = set(topology_tags or [])
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
"
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
337
|
+
prompt_lower = str(plan.prompt or "").lower()
|
|
338
|
+
explicit_home_gateway_prompt = any(
|
|
339
|
+
token in prompt_lower
|
|
340
|
+
for token in ["home gateway", "homegateway", "smart home", "iot "]
|
|
341
|
+
)
|
|
342
|
+
family_hints: list[str] = []
|
|
343
|
+
campus_signal = (
|
|
344
|
+
plan.network_style == "campus"
|
|
345
|
+
or "campus" in prompt_lower
|
|
346
|
+
or "kampus" in prompt_lower
|
|
347
|
+
or bool(plan.department_groups)
|
|
348
|
+
or bool(tags & {"chain", "core_access", "router_on_a_stick", "acl_policy"})
|
|
349
|
+
or bool(
|
|
350
|
+
device_families & {"routers", "switches", "multilayer switches"}
|
|
351
|
+
and capabilities & {"vlan", "router_on_a_stick", "management_vlan", "telnet", "acl", "trunk", "access_port"}
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
home_iot_signal = (
|
|
355
|
+
plan.network_style in {"home_iot", "smart_home"}
|
|
356
|
+
or explicit_home_gateway_prompt
|
|
357
|
+
or plan.wireless_mode == "home_router_edge"
|
|
358
|
+
or bool(capabilities & {"iot", "iot_registration", "iot_control"})
|
|
359
|
+
or bool("iot devices" in device_families)
|
|
360
|
+
)
|
|
361
|
+
wan_signal = (
|
|
362
|
+
plan.network_style == "wan_security"
|
|
363
|
+
or bool(device_families & {"wan/cloud/dsl/cable devices", "security devices"})
|
|
364
|
+
or bool(capabilities & {"vpn", "ipsec", "gre", "ppp", "multilayer_switching", "security_edge"})
|
|
365
|
+
)
|
|
366
|
+
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"})
|
|
367
|
+
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"})
|
|
368
|
+
wireless_advanced_signal = plan.network_style == "wireless_advanced" or bool(capabilities & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"})
|
|
369
|
+
automation_controller_signal = plan.network_style == "automation_controller" or bool(capabilities & {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"})
|
|
370
|
+
voice_collaboration_signal = plan.network_style == "voice_collaboration" or bool(capabilities & {"voip", "ip_phone", "call_manager", "linksys_voice"})
|
|
371
|
+
industrial_iot_signal = plan.network_style == "industrial_iot" or bool(capabilities & {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"})
|
|
372
|
+
wireless_signal = plan.wireless_mode or capabilities & {
|
|
342
373
|
"wireless_ap",
|
|
343
374
|
"wireless_client",
|
|
344
375
|
"wireless_mutation",
|
|
345
376
|
"wireless_client_association",
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
377
|
+
}
|
|
378
|
+
service_signal = (
|
|
379
|
+
requested_services & {"dns", "dhcp", "http", "https", "ftp", "tftp", "email", "syslog", "aaa", "ntp"}
|
|
380
|
+
or capabilities & {"server_dns", "server_dhcp", "server_http", "server_https", "server_ftp", "server_tftp", "server_email", "server_syslog", "server_aaa"}
|
|
381
|
+
or "servers" in device_families
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
if industrial_iot_signal:
|
|
385
|
+
family_hints.append("industrial IoT")
|
|
386
|
+
elif automation_controller_signal:
|
|
387
|
+
family_hints.append("automation/controller")
|
|
388
|
+
elif voice_collaboration_signal:
|
|
389
|
+
family_hints.append("voice/collaboration")
|
|
390
|
+
elif wireless_advanced_signal:
|
|
391
|
+
family_hints.append("advanced wireless")
|
|
392
|
+
elif l2_security_monitoring_signal:
|
|
393
|
+
family_hints.append("L2 security/monitoring")
|
|
394
|
+
elif ipv6_routing_signal:
|
|
395
|
+
family_hints.append("IPv6/routing")
|
|
396
|
+
elif wan_signal:
|
|
397
|
+
family_hints.append("WAN/security edge")
|
|
398
|
+
if campus_signal and not bool(capabilities & {"vpn", "ipsec", "gre", "ppp", "security_edge"}):
|
|
399
|
+
family_hints.append("campus/core")
|
|
400
|
+
elif campus_signal:
|
|
401
|
+
family_hints.append("campus/core")
|
|
402
|
+
if wireless_signal:
|
|
403
|
+
family_hints.append("wireless-heavy")
|
|
404
|
+
if service_signal and requested_services & {"email", "syslog", "aaa", "http", "https", "ftp", "tftp"}:
|
|
405
|
+
family_hints.append("service-heavy")
|
|
406
|
+
elif home_iot_signal:
|
|
407
|
+
family_hints.append("IoT/home gateway")
|
|
408
|
+
if wireless_signal:
|
|
409
|
+
family_hints.append("wireless-heavy")
|
|
410
|
+
elif service_signal:
|
|
411
|
+
family_hints.append("service-heavy")
|
|
412
|
+
elif wireless_signal:
|
|
413
|
+
family_hints.append("wireless-heavy")
|
|
414
|
+
|
|
415
|
+
if not family_hints:
|
|
416
|
+
if service_signal:
|
|
417
|
+
family_hints.append("service-heavy")
|
|
418
|
+
if wireless_signal:
|
|
419
|
+
family_hints.append("wireless-heavy")
|
|
420
|
+
if home_iot_signal:
|
|
421
|
+
family_hints.append("IoT/home gateway")
|
|
422
|
+
if wan_signal:
|
|
423
|
+
family_hints.append("WAN/security edge")
|
|
424
|
+
|
|
425
|
+
for item in family_hints:
|
|
426
|
+
if item not in preferred:
|
|
427
|
+
preferred.append(item)
|
|
352
428
|
return preferred
|
|
353
429
|
|
|
354
430
|
|
|
@@ -361,6 +437,16 @@ def _required_runtime_features_for_plan(plan: IntentPlan) -> list[str]:
|
|
|
361
437
|
features.add("wireless_runtime")
|
|
362
438
|
if capabilities & {"iot", "iot_registration", "iot_control"}:
|
|
363
439
|
features.add("iot_runtime")
|
|
440
|
+
if capabilities & {"vpn", "ipsec", "gre"}:
|
|
441
|
+
features.add("tunnel_runtime")
|
|
442
|
+
if capabilities & {"ppp"}:
|
|
443
|
+
features.add("wan_runtime")
|
|
444
|
+
if capabilities & {"security_edge", "acl", "nat"}:
|
|
445
|
+
features.add("security_runtime")
|
|
446
|
+
if capabilities & {"multilayer_switching"}:
|
|
447
|
+
features.add("multilayer_runtime")
|
|
448
|
+
if capabilities & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}:
|
|
449
|
+
features.add("ipv6_runtime")
|
|
364
450
|
return sorted(features)
|
|
365
451
|
|
|
366
452
|
|
|
@@ -437,6 +523,20 @@ def _candidate_acceptance_penalty(candidate: SampleCandidate, blueprint: dict[st
|
|
|
437
523
|
"wireless_mutation": 18,
|
|
438
524
|
"wireless_client_association": 22,
|
|
439
525
|
"end_device_mutation": 12,
|
|
526
|
+
"iot_registration": 26,
|
|
527
|
+
"iot_control": 24,
|
|
528
|
+
"vpn": 22,
|
|
529
|
+
"ipsec": 22,
|
|
530
|
+
"gre": 20,
|
|
531
|
+
"ppp": 18,
|
|
532
|
+
"security_edge": 20,
|
|
533
|
+
"multilayer_switching": 16,
|
|
534
|
+
"ipv6_slaac": 18,
|
|
535
|
+
"dhcpv6_stateful": 18,
|
|
536
|
+
"ospfv3": 18,
|
|
537
|
+
"eigrp_ipv6": 18,
|
|
538
|
+
"ripng": 16,
|
|
539
|
+
"hsrp": 16,
|
|
440
540
|
}.items():
|
|
441
541
|
if capability in requested_capabilities and capability not in supported_capabilities:
|
|
442
542
|
penalty += penalty_value
|
|
@@ -507,13 +607,108 @@ def _summarize_candidate_pool(
|
|
|
507
607
|
break
|
|
508
608
|
if len(top_rejection_reasons) >= 5:
|
|
509
609
|
continue
|
|
610
|
+
primary_rejection_code = _primary_rejection_code(top_rejection_reasons)
|
|
611
|
+
primary_rejection_layer = _primary_rejection_layer(primary_rejection_code)
|
|
612
|
+
best_rejected_donor_class = (
|
|
613
|
+
next((str(item) for item in list(preferred_archetypes or []) if str(item).strip()), None)
|
|
614
|
+
if any(counts[key] > 0 for key in ("rejected", "filtered"))
|
|
615
|
+
else None
|
|
616
|
+
)
|
|
510
617
|
return {
|
|
511
618
|
"preferred_donor_archetypes": list(preferred_archetypes or []),
|
|
512
619
|
"candidate_counts": counts,
|
|
513
620
|
"best_adjusted_total_score": best_adjusted_score,
|
|
514
621
|
"best_layout_reuse_score": best_layout_reuse_score,
|
|
515
622
|
"top_rejection_reasons": top_rejection_reasons,
|
|
623
|
+
"best_rejected_donor_class": best_rejected_donor_class,
|
|
624
|
+
"primary_rejection_code": primary_rejection_code,
|
|
625
|
+
"primary_rejection_layer": primary_rejection_layer,
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _primary_rejection_code(rejection_reasons: list[str]) -> str | None:
|
|
630
|
+
normalized = [str(item).strip().lower() for item in rejection_reasons if str(item).strip()]
|
|
631
|
+
if any("runtime subtree" in reason for reason in normalized):
|
|
632
|
+
return "runtime_subtree_missing"
|
|
633
|
+
if any("archetype_gap:" in reason or "archetype does not align" in reason for reason in normalized):
|
|
634
|
+
return "archetype_misaligned"
|
|
635
|
+
if any(
|
|
636
|
+
marker in reason
|
|
637
|
+
for reason in normalized
|
|
638
|
+
for marker in (
|
|
639
|
+
"missing_link_pairs:",
|
|
640
|
+
"no reusable link pairs",
|
|
641
|
+
"reuses too little of the requested link skeleton",
|
|
642
|
+
"cannot create new donor link pair",
|
|
643
|
+
"requires donor link reuse",
|
|
644
|
+
"port mismatch",
|
|
645
|
+
"media mismatch",
|
|
646
|
+
"ports/media",
|
|
647
|
+
)
|
|
648
|
+
):
|
|
649
|
+
return "layout_reuse_too_weak"
|
|
650
|
+
if any(
|
|
651
|
+
marker in reason
|
|
652
|
+
for reason in normalized
|
|
653
|
+
for marker in (
|
|
654
|
+
"acceptance penalty",
|
|
655
|
+
"acceptance_gated",
|
|
656
|
+
"acceptance-gated",
|
|
657
|
+
"acceptance fixture",
|
|
658
|
+
"acceptance evidence",
|
|
659
|
+
"acceptance risk",
|
|
660
|
+
)
|
|
661
|
+
):
|
|
662
|
+
return "acceptance_evidence_too_weak"
|
|
663
|
+
return None
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _primary_rejection_layer(primary_rejection_code: str | None) -> str | None:
|
|
667
|
+
if primary_rejection_code == "acceptance_evidence_too_weak":
|
|
668
|
+
return "acceptance"
|
|
669
|
+
if primary_rejection_code in {
|
|
670
|
+
"layout_reuse_too_weak",
|
|
671
|
+
"archetype_misaligned",
|
|
672
|
+
"runtime_subtree_missing",
|
|
673
|
+
}:
|
|
674
|
+
return "donor"
|
|
675
|
+
return None
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _best_rejected_donor_summary(
|
|
679
|
+
best_rejected_donor_class: str | None,
|
|
680
|
+
primary_rejection_code: str | None,
|
|
681
|
+
top_rejection_reasons: list[str],
|
|
682
|
+
) -> str | None:
|
|
683
|
+
if not best_rejected_donor_class and not primary_rejection_code:
|
|
684
|
+
return None
|
|
685
|
+
reason_sentence_map = {
|
|
686
|
+
"layout_reuse_too_weak": "its reusable link skeleton is still too weak for the requested topology",
|
|
687
|
+
"acceptance_evidence_too_weak": "its acceptance evidence is still too weak for prompt-level generate",
|
|
688
|
+
"archetype_misaligned": "its donor shape does not align with the requested scenario archetype",
|
|
689
|
+
"runtime_subtree_missing": "the required runtime subtree is missing from the donor path",
|
|
690
|
+
}
|
|
691
|
+
next_shape_map = {
|
|
692
|
+
"layout_reuse_too_weak": "choose a donor with the same family but a stronger reusable router-switch-service skeleton",
|
|
693
|
+
"acceptance_evidence_too_weak": "choose a donor with explicit acceptance-backed coverage for the same scenario family",
|
|
694
|
+
"archetype_misaligned": "choose a donor whose archetype already matches the requested family",
|
|
695
|
+
"runtime_subtree_missing": "choose a donor that already contains the required runtime subtree and open-state structure",
|
|
516
696
|
}
|
|
697
|
+
donor_class = best_rejected_donor_class or "the closest donor class"
|
|
698
|
+
reason = reason_sentence_map.get(primary_rejection_code or "", "it still did not satisfy strict donor selection")
|
|
699
|
+
next_shape = next_shape_map.get(primary_rejection_code or "", "choose a closer donor before prompt generate")
|
|
700
|
+
if donor_class == "WAN/security edge":
|
|
701
|
+
wan_next_shape_map = {
|
|
702
|
+
"layout_reuse_too_weak": "choose a WAN/security donor with reusable ASA/cloud/serial or tunnel skeleton",
|
|
703
|
+
"acceptance_evidence_too_weak": "choose a WAN/security donor with explicit VPN/IPSec/GRE/PPP acceptance evidence",
|
|
704
|
+
"archetype_misaligned": "choose a donor already tagged as WAN/security edge",
|
|
705
|
+
"runtime_subtree_missing": "choose a donor that already contains the WAN/security runtime subtree",
|
|
706
|
+
}
|
|
707
|
+
next_shape = wan_next_shape_map.get(primary_rejection_code or "", "choose a closer WAN/security donor before prompt generate")
|
|
708
|
+
summary = f"Best rejected donor class {donor_class} was closest, but {reason}; {next_shape}."
|
|
709
|
+
if top_rejection_reasons:
|
|
710
|
+
summary = f"{summary} Top rejection signal: {top_rejection_reasons[0]}."
|
|
711
|
+
return summary
|
|
517
712
|
|
|
518
713
|
|
|
519
714
|
def _load_fixture_corpus() -> dict[str, object]:
|
|
@@ -699,6 +894,10 @@ def _filter_candidates_for_blueprint(
|
|
|
699
894
|
filter_reasons.append("sample lacks donor-backed support for requested wireless mutation")
|
|
700
895
|
if "end_device_mutation" in required_capabilities and "end_device_mutation" not in supported_capabilities:
|
|
701
896
|
filter_reasons.append("sample lacks donor-backed support for requested end-device mutation")
|
|
897
|
+
if "iot_registration" in required_capabilities and "iot_registration" not in supported_capabilities:
|
|
898
|
+
filter_reasons.append("sample lacks donor-backed support for requested IoT registration")
|
|
899
|
+
if "iot_control" in required_capabilities and "iot_control" not in supported_capabilities:
|
|
900
|
+
filter_reasons.append("sample lacks donor-backed support for requested IoT control")
|
|
702
901
|
if adjusted_total_score <= 0:
|
|
703
902
|
filter_reasons.append("acceptance penalty reduced the donor score below zero")
|
|
704
903
|
if filter_reasons:
|
|
@@ -2472,8 +2671,11 @@ def _augment_coverage_gap_actions(
|
|
|
2472
2671
|
actions.append("For service-heavy prompts, prefer a donor that already contains the required server service family and core server layout.")
|
|
2473
2672
|
if scenario_family == "home_iot" and scenario_status in {"donor_limited", "acceptance_gated", "unsupported"}:
|
|
2474
2673
|
actions.append("For home IoT prompts, prefer a donor with Home Gateway plus existing IoT registration/control structure.")
|
|
2674
|
+
actions.append("For donor-backed Home IoT generate, explicitly name the thing, gateway/server target, and wireless client or SSID targets.")
|
|
2475
2675
|
if scenario_family == "wan_security_edge" and scenario_status in {"donor_limited", "acceptance_gated", "unsupported"}:
|
|
2476
2676
|
actions.append("For WAN/security prompts, prefer a donor with reusable serial/WAN or security-edge skeleton before generating.")
|
|
2677
|
+
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"}:
|
|
2678
|
+
actions.append("This feature family is atlas/report-first; keep it out of strict generate until donor-backed proof and acceptance fixtures exist.")
|
|
2477
2679
|
updated["recommended_next_actions"] = list(dict.fromkeys(actions))
|
|
2478
2680
|
return updated
|
|
2479
2681
|
|
|
@@ -2524,12 +2726,26 @@ def _scenario_generate_decision(
|
|
|
2524
2726
|
"service_heavy": "service-heavy",
|
|
2525
2727
|
"home_iot": "home IoT",
|
|
2526
2728
|
"wan_security_edge": "WAN/security edge",
|
|
2729
|
+
"ipv6_routing": "IPv6/routing",
|
|
2730
|
+
"l2_security_monitoring": "L2 security/monitoring",
|
|
2731
|
+
"wireless_advanced": "advanced wireless",
|
|
2732
|
+
"automation_controller": "automation/controller",
|
|
2733
|
+
"voice_collaboration": "voice/collaboration",
|
|
2734
|
+
"industrial_iot": "industrial IoT",
|
|
2735
|
+
"physical_media_device": "physical/media device",
|
|
2527
2736
|
}
|
|
2528
2737
|
expected_archetype_map = {
|
|
2529
2738
|
"campus": "campus/core",
|
|
2530
2739
|
"service_heavy": "service-heavy",
|
|
2531
2740
|
"home_iot": "IoT/home gateway",
|
|
2532
2741
|
"wan_security_edge": "WAN/security edge",
|
|
2742
|
+
"ipv6_routing": "IPv6/routing",
|
|
2743
|
+
"l2_security_monitoring": "L2 security/monitoring",
|
|
2744
|
+
"wireless_advanced": "advanced wireless",
|
|
2745
|
+
"automation_controller": "automation/controller",
|
|
2746
|
+
"voice_collaboration": "voice/collaboration",
|
|
2747
|
+
"industrial_iot": "industrial IoT",
|
|
2748
|
+
"physical_media_device": "physical/media device",
|
|
2533
2749
|
}
|
|
2534
2750
|
family_label = family_label_map.get(family, family)
|
|
2535
2751
|
blocking_reasons: list[str] = []
|
|
@@ -2543,6 +2759,13 @@ def _scenario_generate_decision(
|
|
|
2543
2759
|
expected_archetype = expected_archetype_map.get(family)
|
|
2544
2760
|
sample_archetypes = [str(item) for item in list((selected_donor_summary or {}).get("sample_archetypes", [])) if str(item).strip()]
|
|
2545
2761
|
donor_graph_summary = dict((selected_donor_summary or {}).get("donor_graph_summary") or {})
|
|
2762
|
+
best_rejected_donor_class = str((donor_selection_summary or {}).get("best_rejected_donor_class") or "").strip() or None
|
|
2763
|
+
primary_rejection_code = str((donor_selection_summary or {}).get("primary_rejection_code") or "").strip() or None
|
|
2764
|
+
best_rejected_donor_summary = _best_rejected_donor_summary(
|
|
2765
|
+
best_rejected_donor_class,
|
|
2766
|
+
primary_rejection_code,
|
|
2767
|
+
[str(item) for item in list((donor_selection_summary or {}).get("top_rejection_reasons", [])) if str(item).strip()],
|
|
2768
|
+
)
|
|
2546
2769
|
if expected_archetype and sample_archetypes:
|
|
2547
2770
|
aligned = expected_archetype in sample_archetypes
|
|
2548
2771
|
decision["selected_donor_aligned"] = aligned
|
|
@@ -2590,6 +2813,8 @@ def _scenario_generate_decision(
|
|
|
2590
2813
|
blocking_reasons.append(
|
|
2591
2814
|
f"Scenario '{family_label}' depends on donor-limited safe-open coverage, but no compatible donor was selected."
|
|
2592
2815
|
)
|
|
2816
|
+
if best_rejected_donor_summary:
|
|
2817
|
+
notes.append(best_rejected_donor_summary)
|
|
2593
2818
|
layout_status = str(donor_graph_summary.get("layout_reuse_status") or "").strip()
|
|
2594
2819
|
pair_coverage = donor_graph_summary.get("reusable_pair_coverage")
|
|
2595
2820
|
if layout_status == "weak":
|
|
@@ -2600,6 +2825,18 @@ def _scenario_generate_decision(
|
|
|
2600
2825
|
blocking_reasons.append(
|
|
2601
2826
|
f"Scenario '{family_label}' donor selection has no reusable link-pair coverage for prompt generate."
|
|
2602
2827
|
)
|
|
2828
|
+
if primary_rejection_code == "archetype_misaligned":
|
|
2829
|
+
blocking_reasons.append(
|
|
2830
|
+
f"Scenario '{family_label}' best donor class is still archetype-misaligned with the requested prompt shape."
|
|
2831
|
+
)
|
|
2832
|
+
elif primary_rejection_code == "runtime_subtree_missing":
|
|
2833
|
+
blocking_reasons.append(
|
|
2834
|
+
f"Scenario '{family_label}' donor candidates are missing required runtime subtree coverage for strict reuse."
|
|
2835
|
+
)
|
|
2836
|
+
elif primary_rejection_code == "acceptance_evidence_too_weak":
|
|
2837
|
+
blocking_reasons.append(
|
|
2838
|
+
f"Scenario '{family_label}' donor candidates still lack strong enough acceptance evidence for strict prompt generate."
|
|
2839
|
+
)
|
|
2603
2840
|
if layout_status in {"strong", "partial"} and isinstance(pair_coverage, int) and pair_coverage > 0:
|
|
2604
2841
|
notes.append(f"selected donor provides {pair_coverage}% reusable link-pair coverage ({layout_status})")
|
|
2605
2842
|
if selected_donor_summary and not blocking_reasons:
|
|
@@ -2668,12 +2905,6 @@ def _scenario_acceptance_summary(
|
|
|
2668
2905
|
donor_state = "candidate_pool_blocked"
|
|
2669
2906
|
if donor_state == "not_selected" and int(candidate_counts.get("selected", 0) or 0) > 0:
|
|
2670
2907
|
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
2908
|
key_reasons = [str(item) for item in decision.get("blocking_reasons", []) if str(item).strip()]
|
|
2678
2909
|
if not key_reasons:
|
|
2679
2910
|
key_reasons = [str(item) for item in decision.get("notes", []) if str(item).strip()]
|
|
@@ -2682,11 +2913,19 @@ def _scenario_acceptance_summary(
|
|
|
2682
2913
|
None,
|
|
2683
2914
|
)
|
|
2684
2915
|
top_rejection_reasons = [str(item) for item in list((donor_selection_summary or {}).get("top_rejection_reasons", [])) if str(item).strip()]
|
|
2916
|
+
primary_rejection_code = str((donor_selection_summary or {}).get("primary_rejection_code") or "").strip() or _primary_rejection_code(top_rejection_reasons)
|
|
2917
|
+
primary_rejection_layer = str((donor_selection_summary or {}).get("primary_rejection_layer") or "").strip() or _primary_rejection_layer(primary_rejection_code)
|
|
2918
|
+
best_rejected_donor_class = str((donor_selection_summary or {}).get("best_rejected_donor_class") or "").strip() or None
|
|
2919
|
+
selection_failure_type = None
|
|
2685
2920
|
if not selected_donor_summary:
|
|
2686
|
-
if
|
|
2921
|
+
if primary_rejection_code == "archetype_misaligned":
|
|
2687
2922
|
selection_failure_type = "viable_donor_found_but_archetype_misaligned"
|
|
2688
|
-
elif
|
|
2923
|
+
elif primary_rejection_code == "runtime_subtree_missing":
|
|
2689
2924
|
selection_failure_type = "viable_donor_found_but_runtime_subtree_missing"
|
|
2925
|
+
elif donor_state in {"candidate_pool_blocked", "selection_pending"}:
|
|
2926
|
+
selection_failure_type = "viable_donor_found_but_acceptance_weak"
|
|
2927
|
+
else:
|
|
2928
|
+
selection_failure_type = "no_viable_donor_found"
|
|
2690
2929
|
critical_capability_parity, critical_parity_mismatches = _critical_capability_parity(coverage_gap)
|
|
2691
2930
|
best_available_donor_class = next(
|
|
2692
2931
|
(
|
|
@@ -2696,11 +2935,39 @@ def _scenario_acceptance_summary(
|
|
|
2696
2935
|
),
|
|
2697
2936
|
None,
|
|
2698
2937
|
)
|
|
2938
|
+
if best_available_donor_class is None:
|
|
2939
|
+
family_donor_class_map = {
|
|
2940
|
+
"campus": "campus/core",
|
|
2941
|
+
"service_heavy": "service-heavy",
|
|
2942
|
+
"home_iot": "IoT/home gateway",
|
|
2943
|
+
"wan_security_edge": "WAN/security edge",
|
|
2944
|
+
"ipv6_routing": "IPv6/routing",
|
|
2945
|
+
"l2_security_monitoring": "L2 security/monitoring",
|
|
2946
|
+
"wireless_advanced": "advanced wireless",
|
|
2947
|
+
"automation_controller": "automation/controller",
|
|
2948
|
+
"voice_collaboration": "voice/collaboration",
|
|
2949
|
+
"industrial_iot": "industrial IoT",
|
|
2950
|
+
"physical_media_device": "physical/media device",
|
|
2951
|
+
}
|
|
2952
|
+
best_available_donor_class = family_donor_class_map.get(str(readiness.get("family") or "").strip())
|
|
2953
|
+
if selected_donor_summary:
|
|
2954
|
+
best_rejected_donor_class = None
|
|
2955
|
+
primary_rejection_code = None
|
|
2956
|
+
primary_rejection_layer = None
|
|
2957
|
+
elif best_rejected_donor_class is None:
|
|
2958
|
+
best_rejected_donor_class = best_available_donor_class
|
|
2699
2959
|
remediation_steps = [
|
|
2700
2960
|
str(item)
|
|
2701
2961
|
for item in list(coverage_gap.get("recommended_next_actions", []))
|
|
2702
2962
|
if str(item).strip()
|
|
2703
2963
|
][:3]
|
|
2964
|
+
best_rejected_donor_summary = _best_rejected_donor_summary(
|
|
2965
|
+
best_rejected_donor_class,
|
|
2966
|
+
primary_rejection_code,
|
|
2967
|
+
top_rejection_reasons,
|
|
2968
|
+
)
|
|
2969
|
+
if best_rejected_donor_summary:
|
|
2970
|
+
remediation_steps = [best_rejected_donor_summary, *remediation_steps][:3]
|
|
2704
2971
|
missing_donor_classes = [best_available_donor_class] if selection_failure_type == "viable_donor_found_but_archetype_misaligned" and best_available_donor_class else []
|
|
2705
2972
|
critical_runtime_requirements: list[str] = []
|
|
2706
2973
|
if runtime_blocked:
|
|
@@ -2725,10 +2992,14 @@ def _scenario_acceptance_summary(
|
|
|
2725
2992
|
"next_best_action": next_best_action,
|
|
2726
2993
|
"remediation_steps": remediation_steps,
|
|
2727
2994
|
"best_available_donor_class": best_available_donor_class,
|
|
2995
|
+
"best_rejected_donor_class": best_rejected_donor_class,
|
|
2996
|
+
"best_rejected_donor_summary": best_rejected_donor_summary,
|
|
2728
2997
|
"missing_donor_classes": missing_donor_classes,
|
|
2729
2998
|
"critical_runtime_requirements": critical_runtime_requirements,
|
|
2730
2999
|
"decision_confidence": decision.get("decision_confidence"),
|
|
2731
3000
|
"blocking_layer": decision.get("blocking_layer"),
|
|
3001
|
+
"primary_rejection_layer": primary_rejection_layer,
|
|
3002
|
+
"primary_rejection_code": primary_rejection_code,
|
|
2732
3003
|
}
|
|
2733
3004
|
|
|
2734
3005
|
|
|
@@ -2808,6 +3079,7 @@ def _scenario_matrix_row(
|
|
|
2808
3079
|
"decision_confidence": summary.get("decision_confidence"),
|
|
2809
3080
|
"blocking_layer": summary.get("blocking_layer"),
|
|
2810
3081
|
"best_available_donor_class": summary.get("best_available_donor_class"),
|
|
3082
|
+
"primary_rejection_code": summary.get("primary_rejection_code"),
|
|
2811
3083
|
"remediation_hint": next((str(item) for item in list(summary.get("remediation_steps", []) or []) if str(item).strip()), None),
|
|
2812
3084
|
}
|
|
2813
3085
|
|
|
@@ -2984,7 +3256,7 @@ def generate_from_prompt(
|
|
|
2984
3256
|
)
|
|
2985
3257
|
coverage_gap = _augment_coverage_gap_actions(
|
|
2986
3258
|
coverage_gap,
|
|
2987
|
-
donor_blocking_reason=
|
|
3259
|
+
donor_blocking_reason=_inspect_packet_tracer_compatibility_donor_cached().blocking_reason,
|
|
2988
3260
|
)
|
|
2989
3261
|
scenario_generate_decision = _scenario_generate_decision(coverage_gap)
|
|
2990
3262
|
prepared_plan.remote_search_results = remote_results
|
|
@@ -3084,7 +3356,7 @@ def _explain_plan_payload(
|
|
|
3084
3356
|
)
|
|
3085
3357
|
plan = _apply_prompt_compatibility_requirements(raw_plan, resolved_donor_roots)
|
|
3086
3358
|
plan.remote_search_results = remote_results
|
|
3087
|
-
donor_details =
|
|
3359
|
+
donor_details = _inspect_packet_tracer_compatibility_donor_cached()
|
|
3088
3360
|
compat_donor, compat_donor_version = donor_details.resolved_path, donor_details.donor_version
|
|
3089
3361
|
topology_tags = _topology_tags_for_plan(plan, _choose_topology_archetype(plan))
|
|
3090
3362
|
result: dict[str, object] = {
|
|
@@ -3213,8 +3485,22 @@ def _explain_plan_payload(
|
|
|
3213
3485
|
try:
|
|
3214
3486
|
if evaluation is None:
|
|
3215
3487
|
raise PlanningError("Prompt plan is incomplete; generation was skipped.", prepared)
|
|
3216
|
-
adapted_plan, donor_archetype, donor_root,
|
|
3488
|
+
adapted_plan, donor_archetype, donor_root, selected_candidate = evaluation
|
|
3217
3489
|
safe_plan, profiled_plan = _apply_safe_open_profile(donor_root, adapted_plan)
|
|
3490
|
+
matrix_hits, coverage_gap, blueprint_plan = _build_support_reports(
|
|
3491
|
+
profiled_plan,
|
|
3492
|
+
blueprint=blueprint,
|
|
3493
|
+
cisco_ranked=cisco_ranked,
|
|
3494
|
+
curated_ranked=curated_ranked,
|
|
3495
|
+
reference_catalog=reference_catalog,
|
|
3496
|
+
selected_donor=selected_candidate.sample.relative_path,
|
|
3497
|
+
)
|
|
3498
|
+
coverage_gap = _augment_coverage_gap_actions(
|
|
3499
|
+
coverage_gap,
|
|
3500
|
+
donor_diagnostics=diagnostics,
|
|
3501
|
+
donor_selection_summary=result["donor_selection_summary"],
|
|
3502
|
+
donor_blocking_reason=donor_details.blocking_reason,
|
|
3503
|
+
)
|
|
3218
3504
|
profiled_plan.remote_search_results = remote_results
|
|
3219
3505
|
profiled_plan.capability_matrix_hits = matrix_hits
|
|
3220
3506
|
profiled_plan.coverage_gap_report = coverage_gap
|
|
@@ -3269,6 +3555,11 @@ def _explain_plan_payload(
|
|
|
3269
3555
|
result["scenario_acceptance_summary"],
|
|
3270
3556
|
selected_donor_summary=result["selected_donor_summary"],
|
|
3271
3557
|
)
|
|
3558
|
+
result["capability_matrix_hits"] = matrix_hits
|
|
3559
|
+
result["unsupported_capabilities"] = coverage_gap.get("unsupported_capabilities", [])
|
|
3560
|
+
result["coverage_gaps"] = coverage_gap
|
|
3561
|
+
result["capability_parity"] = list(coverage_gap.get("capability_parity", []))
|
|
3562
|
+
result["blueprint_plan"] = blueprint_plan
|
|
3272
3563
|
except PlanningError as exc:
|
|
3273
3564
|
if result["donor_rejection_reasons"]:
|
|
3274
3565
|
for item in result["donor_rejection_reasons"]:
|
|
@@ -3377,7 +3668,7 @@ def inventory_pkt(
|
|
|
3377
3668
|
root = ET.fromstring(decode_pkt_modern(pkt_path.read_bytes()))
|
|
3378
3669
|
payload = inventory_root(root)
|
|
3379
3670
|
workspace = inspect_workspace_integrity(root)
|
|
3380
|
-
donor_details =
|
|
3671
|
+
donor_details = _inspect_packet_tracer_compatibility_donor_cached()
|
|
3381
3672
|
compat_donor, compat_donor_version = donor_details.resolved_path, donor_details.donor_version
|
|
3382
3673
|
payload["workspace_validation"] = {
|
|
3383
3674
|
"workspace_mode": workspace.workspace_mode,
|
|
@@ -3635,6 +3926,10 @@ def compare_scenarios(
|
|
|
3635
3926
|
print(rendered)
|
|
3636
3927
|
|
|
3637
3928
|
|
|
3929
|
+
def feature_gap_report() -> None:
|
|
3930
|
+
print(json.dumps(build_feature_gap_report(), indent=2, ensure_ascii=False))
|
|
3931
|
+
|
|
3932
|
+
|
|
3638
3933
|
def main() -> None:
|
|
3639
3934
|
parser = argparse.ArgumentParser(description="Generate or inspect Cisco Packet Tracer 9.0 .pkt files")
|
|
3640
3935
|
parser.add_argument("--blueprint", help="Path to the topology blueprint JSON")
|
|
@@ -3658,6 +3953,7 @@ def main() -> None:
|
|
|
3658
3953
|
parser.add_argument("--max-remote-results", type=int, default=10, help="Maximum number of remote search results to fetch/import")
|
|
3659
3954
|
parser.add_argument("--blueprint-out", help="Optional JSON output path for the generated blueprint plan or refusal blueprint")
|
|
3660
3955
|
parser.add_argument("--coverage-report", action="store_true", help="Print the aggregated capability coverage matrix")
|
|
3956
|
+
parser.add_argument("--feature-gap-report", action="store_true", help="Print the Packet Tracer feature gap atlas report")
|
|
3661
3957
|
parser.add_argument("--inventory-capabilities", action="store_true", help="Include inferred capability inventory when using --inventory")
|
|
3662
3958
|
parser.add_argument("--inventory-out", help="Optional JSON output path when using --inventory")
|
|
3663
3959
|
parser.add_argument("--matrix-out", help="Optional JSON output path when using --compare-scenarios")
|
|
@@ -3725,6 +4021,9 @@ def main() -> None:
|
|
|
3725
4021
|
if args.coverage_report:
|
|
3726
4022
|
coverage_report(reference_roots, donor_roots, device_family=args.device_family)
|
|
3727
4023
|
return
|
|
4024
|
+
if args.feature_gap_report:
|
|
4025
|
+
feature_gap_report()
|
|
4026
|
+
return
|
|
3728
4027
|
if args.decode:
|
|
3729
4028
|
if not args.xml_out:
|
|
3730
4029
|
parser.error("--decode requires --xml-out")
|