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.
Files changed (57) hide show
  1. package/CHANGELOG.md +42 -4
  2. package/README.md +237 -88
  3. package/docs/campus-donor-proof.md +103 -0
  4. package/docs/curated-donor-registry.md +26 -0
  5. package/docs/github-launch-ops-0.2.1.md +45 -0
  6. package/docs/github-launch-ops-0.2.2.md +42 -0
  7. package/docs/github-metadata.md +23 -6
  8. package/docs/hero-demo-plan.md +18 -10
  9. package/docs/home-iot-donor-proof.md +64 -0
  10. package/docs/launch-announcement-0.2.1.md +15 -0
  11. package/docs/launch-announcement-0.2.2.md +15 -0
  12. package/docs/packet-tracer-feature-gap-atlas.md +87 -0
  13. package/docs/post-launch-follow-up.md +39 -0
  14. package/docs/publish-preview-roadmap.md +20 -17
  15. package/docs/release-checklist.md +22 -12
  16. package/docs/{release-notes-0.2.0.md → release-notes-0.2.1.md} +7 -6
  17. package/docs/release-notes-0.2.2.md +26 -0
  18. package/docs/runtime-truth.md +31 -0
  19. package/docs/wan-security-donor-proof.md +41 -0
  20. package/docs/wireless-advanced-proof.md +60 -0
  21. package/examples/README.md +13 -10
  22. package/examples/gallery.md +10 -4
  23. package/examples/index.json +3 -3
  24. package/package.json +44 -16
  25. package/references/curated-donor-registry.json +5 -2
  26. package/references/packettracer-feature-atlas.json +133 -0
  27. package/references/scenario-fixture-corpus.json +1 -0
  28. package/scripts/build_examples_index.py +27 -16
  29. package/scripts/coverage_matrix.py +559 -23
  30. package/scripts/feature_atlas.py +229 -0
  31. package/scripts/generate_pkt.py +330 -31
  32. package/scripts/intent_parser.py +278 -7
  33. package/scripts/pkt_editor.py +174 -0
  34. package/scripts/runtime_doctor.py +83 -10
  35. package/scripts/sample_catalog.py +210 -5
  36. package/docs/screenshots/.gitkeep +0 -1
  37. package/docs/screenshots/packet-tracer-topology-cropped.png +0 -0
  38. package/scripts/__pycache__/build_examples_index.cpython-314.pyc +0 -0
  39. package/scripts/__pycache__/build_sample_catalog.cpython-314.pyc +0 -0
  40. package/scripts/__pycache__/coverage_matrix.cpython-314.pyc +0 -0
  41. package/scripts/__pycache__/donor_diagnostics.cpython-314.pyc +0 -0
  42. package/scripts/__pycache__/generate_pkt.cpython-314.pyc +0 -0
  43. package/scripts/__pycache__/install_skill.cpython-314.pyc +0 -0
  44. package/scripts/__pycache__/intent_parser.cpython-314.pyc +0 -0
  45. package/scripts/__pycache__/packet_tracer_env.cpython-314.pyc +0 -0
  46. package/scripts/__pycache__/pkt_builder.cpython-314.pyc +0 -0
  47. package/scripts/__pycache__/pkt_codec.cpython-314.pyc +0 -0
  48. package/scripts/__pycache__/pkt_editor.cpython-314.pyc +0 -0
  49. package/scripts/__pycache__/pkt_transformer.cpython-314.pyc +0 -0
  50. package/scripts/__pycache__/remote_search.cpython-314.pyc +0 -0
  51. package/scripts/__pycache__/runtime_doctor.cpython-314.pyc +0 -0
  52. package/scripts/__pycache__/sample_catalog.cpython-314.pyc +0 -0
  53. package/scripts/__pycache__/sample_selector.cpython-314.pyc +0 -0
  54. package/scripts/__pycache__/twofish_diagnostics.cpython-314.pyc +0 -0
  55. package/scripts/__pycache__/twofish_runtime.cpython-314.pyc +0 -0
  56. package/scripts/__pycache__/workspace_repair.cpython-314.pyc +0 -0
  57. package/scripts/vendor/__pycache__/twofish.cpython-314.pyc +0 -0
@@ -0,0 +1,229 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from coverage_matrix import CAPABILITY_PROVIDER_FAMILIES
8
+ from intent_parser import CAPABILITY_PATTERNS
9
+ from sample_catalog import CAPABILITY_KEYWORDS
10
+
11
+
12
+ ROOT = Path(__file__).resolve().parents[1]
13
+ DEFAULT_FEATURE_ATLAS = ROOT / "references" / "packettracer-feature-atlas.json"
14
+ DEFAULT_SAMPLE_CATALOG = ROOT / "references" / "packettracer-sample-catalog.json"
15
+ EDITOR_TEST_PATH = ROOT / "tests" / "test_pkt_editor.py"
16
+ EDITOR_TEST_ALIASES = {
17
+ "ipv6_slaac": ["set_ipv6_slaac", "slaac on"],
18
+ "dhcpv6_stateful": ["set_dhcpv6_pool", "dhcpv6 pool"],
19
+ "ospfv3": ["set_ospfv3_interface", "ospfv3"],
20
+ "eigrp_ipv6": ["set_eigrp_ipv6_interface", "eigrp ipv6"],
21
+ "ripng": ["set_ripng_interface", "ripng"],
22
+ "hsrp": ["set_hsrp_ipv6", "hsrp"],
23
+ "dhcp_snooping": ["set_dhcp_snooping", "dhcp snooping"],
24
+ "dai": ["set_dai", "dynamic arp inspection"],
25
+ "lldp": ["set_lldp", "lldp enable"],
26
+ "rep": ["set_rep", "rep segment"],
27
+ "snmp": ["set_snmp_community", "snmp community"],
28
+ "netflow": ["set_netflow", "netflow destination"],
29
+ "span": ["set_span", "monitor session"],
30
+ "port_security": ["set_port_security", "port-security"],
31
+ "wep": ["security wep", "WEP_KEY"],
32
+ "wpa_enterprise": ["wpa-enterprise", "wpa2-enterprise", "802.1x", "radius_server"],
33
+ }
34
+
35
+ STATUS_ORDER = [
36
+ "not_mapped",
37
+ "inventory_known",
38
+ "report_supported",
39
+ "edit_proven",
40
+ "donor_backed_ready",
41
+ "generate_ready",
42
+ ]
43
+ STATUS_RANK = {status: index for index, status in enumerate(STATUS_ORDER)}
44
+
45
+
46
+ def load_feature_atlas(path: Path = DEFAULT_FEATURE_ATLAS) -> dict[str, Any]:
47
+ return json.loads(path.read_text(encoding="utf-8"))
48
+
49
+
50
+ def load_sample_catalog(path: Path = DEFAULT_SAMPLE_CATALOG) -> list[dict[str, Any]]:
51
+ payload = json.loads(path.read_text(encoding="utf-8"))
52
+ if isinstance(payload, list):
53
+ return [item for item in payload if isinstance(item, dict)]
54
+ if isinstance(payload, dict):
55
+ for key in ("samples", "items"):
56
+ items = payload.get(key)
57
+ if isinstance(items, list):
58
+ return [item for item in items if isinstance(item, dict)]
59
+ return []
60
+
61
+
62
+ def _status_at_most(status: str, ceiling: str) -> str:
63
+ if STATUS_RANK[status] <= STATUS_RANK[ceiling]:
64
+ return status
65
+ return ceiling
66
+
67
+
68
+ def _sample_matches(feature: dict[str, Any], sample: dict[str, Any]) -> bool:
69
+ rel = str(sample.get("relative_path") or "").replace("\\", "/").lower()
70
+ tags = {str(tag).lower() for tag in sample.get("capability_tags", [])}
71
+ capability = str(feature.get("capability") or feature.get("id") or "").lower()
72
+ if capability in tags:
73
+ return True
74
+ for keyword in feature.get("sample_path_keywords", []):
75
+ if str(keyword).lower() in rel:
76
+ return True
77
+ return False
78
+
79
+
80
+ def _sample_hits(feature: dict[str, Any], samples: list[dict[str, Any]]) -> list[str]:
81
+ hits: list[str] = []
82
+ for sample in samples:
83
+ if _sample_matches(feature, sample):
84
+ rel = str(sample.get("relative_path") or "")
85
+ if rel:
86
+ hits.append(rel)
87
+ return sorted(dict.fromkeys(hits))
88
+
89
+
90
+ def _decoded_sample_hits(feature: dict[str, Any], samples: list[dict[str, Any]]) -> list[str]:
91
+ hits: list[str] = []
92
+ for sample in samples:
93
+ if not _sample_matches(feature, sample):
94
+ continue
95
+ if not str(sample.get("version") or "").strip():
96
+ continue
97
+ rel = str(sample.get("relative_path") or "")
98
+ if rel:
99
+ hits.append(rel)
100
+ return sorted(dict.fromkeys(hits))
101
+
102
+
103
+ def _editor_test_mentions(capability: str) -> bool:
104
+ if not EDITOR_TEST_PATH.exists():
105
+ return False
106
+ text = EDITOR_TEST_PATH.read_text(encoding="utf-8").lower()
107
+ needles = [capability.lower(), *[alias.lower() for alias in EDITOR_TEST_ALIASES.get(capability, [])]]
108
+ return any(needle in text for needle in needles)
109
+
110
+
111
+ def _feature_status(feature: dict[str, Any], evidence: dict[str, Any]) -> str:
112
+ capability = str(feature.get("capability") or feature.get("id") or "")
113
+ ceiling = str(feature.get("support_ceiling") or "report_supported")
114
+ if evidence["editor_roundtrip_test"]:
115
+ raw_status = "edit_proven"
116
+ elif evidence["parser_pattern"] and (evidence["sample_count"] or evidence["catalog_keyword"] or evidence["coverage_provider"]):
117
+ raw_status = "report_supported"
118
+ elif evidence["sample_count"] or evidence["catalog_keyword"] or evidence["coverage_provider"]:
119
+ raw_status = "inventory_known"
120
+ else:
121
+ raw_status = "not_mapped"
122
+ if capability in {"vlan", "trunk", "access_port", "router_on_a_stick", "management_vlan", "telnet", "acl", "dhcp_pool"}:
123
+ raw_status = "generate_ready"
124
+ return _status_at_most(raw_status, ceiling)
125
+
126
+
127
+ def _next_safe_action(status: str, feature: dict[str, Any]) -> str:
128
+ label = str(feature.get("label") or feature.get("id"))
129
+ if status == "not_mapped":
130
+ return f"Add parser/catalog recognition for {label}, then keep it report-only until donor evidence exists."
131
+ if status == "inventory_known":
132
+ return f"Add prompt parsing and scenario-family report coverage for {label}."
133
+ if status == "report_supported":
134
+ return f"Keep {label} report-only; add donor-backed proof before edit/generate readiness."
135
+ if status == "edit_proven":
136
+ return f"Promote {label} only after selected-donor and acceptance fixture coverage exists."
137
+ if status == "donor_backed_ready":
138
+ return f"Add constrained acceptance fixtures before claiming broad generate for {label}."
139
+ return f"Keep regression coverage for {label}."
140
+
141
+
142
+ def build_feature_gap_report(
143
+ atlas_path: Path = DEFAULT_FEATURE_ATLAS,
144
+ sample_catalog_path: Path = DEFAULT_SAMPLE_CATALOG,
145
+ ) -> dict[str, Any]:
146
+ atlas = load_feature_atlas(atlas_path)
147
+ samples = load_sample_catalog(sample_catalog_path)
148
+ groups: list[dict[str, Any]] = []
149
+ status_counts = {status: 0 for status in STATUS_ORDER}
150
+ total_features = 0
151
+ mapped_features = 0
152
+ sample_feature_count = 0
153
+
154
+ for group in atlas.get("feature_groups", []):
155
+ group_features: list[dict[str, Any]] = []
156
+ group_status_counts = {status: 0 for status in STATUS_ORDER}
157
+ for feature in group.get("features", []):
158
+ total_features += 1
159
+ capability = str(feature.get("capability") or feature.get("id") or "")
160
+ hits = _sample_hits(feature, samples)
161
+ decoded_hits = _decoded_sample_hits(feature, samples)
162
+ evidence = {
163
+ "parser_pattern": capability in CAPABILITY_PATTERNS,
164
+ "catalog_keyword": capability in CAPABILITY_KEYWORDS,
165
+ "coverage_provider": capability in CAPABILITY_PROVIDER_FAMILIES,
166
+ "editor_roundtrip_test": _editor_test_mentions(capability),
167
+ "sample_count": len(hits),
168
+ "sample_examples": hits[:5],
169
+ "sample_path_count": len(hits),
170
+ "decode_verified_sample_count": len(decoded_hits),
171
+ "decode_verified_examples": decoded_hits[:5],
172
+ }
173
+ status = _feature_status(feature, evidence)
174
+ if status != "not_mapped":
175
+ mapped_features += 1
176
+ if hits:
177
+ sample_feature_count += 1
178
+ status_counts[status] += 1
179
+ group_status_counts[status] += 1
180
+ group_features.append(
181
+ {
182
+ "id": feature.get("id"),
183
+ "capability": capability,
184
+ "label": feature.get("label"),
185
+ "status": status,
186
+ "support_ceiling": feature.get("support_ceiling", "report_supported"),
187
+ "evidence": evidence,
188
+ "next_safe_action": _next_safe_action(status, feature),
189
+ }
190
+ )
191
+ groups.append(
192
+ {
193
+ "id": group.get("id"),
194
+ "label": group.get("label"),
195
+ "scenario_family": group.get("scenario_family"),
196
+ "wave": group.get("wave"),
197
+ "status_counts": group_status_counts,
198
+ "features": group_features,
199
+ }
200
+ )
201
+
202
+ missing_groups = sorted(
203
+ (
204
+ {
205
+ "id": group["id"],
206
+ "label": group["label"],
207
+ "not_generate_ready_count": sum(
208
+ count for status, count in group["status_counts"].items() if status != "generate_ready"
209
+ ),
210
+ }
211
+ for group in groups
212
+ ),
213
+ key=lambda item: (-int(item["not_generate_ready_count"]), str(item["id"])),
214
+ )
215
+
216
+ return {
217
+ "atlas_version": atlas.get("atlas_version", "unknown"),
218
+ "target_packet_tracer_version": atlas.get("target_packet_tracer_version", "unknown"),
219
+ "support_policy": atlas.get("support_policy", ""),
220
+ "status_contract": STATUS_ORDER,
221
+ "packet_tracer_sample_count": len(samples),
222
+ "total_feature_count": total_features,
223
+ "mapped_feature_count": mapped_features,
224
+ "unmapped_feature_count": total_features - mapped_features,
225
+ "sample_backed_feature_count": sample_feature_count,
226
+ "status_counts": status_counts,
227
+ "top_missing_feature_families": missing_groups[:8],
228
+ "groups": groups,
229
+ }