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/remote_search.py
CHANGED
|
@@ -14,6 +14,16 @@ import zipfile
|
|
|
14
14
|
from intent_parser import IntentPlan
|
|
15
15
|
|
|
16
16
|
GITHUB_SEARCH_API = "https://api.github.com/search/repositories"
|
|
17
|
+
PERMISSIVE_LICENSES = {
|
|
18
|
+
"APACHE-2.0",
|
|
19
|
+
"BSD-2-CLAUSE",
|
|
20
|
+
"BSD-3-CLAUSE",
|
|
21
|
+
"CC0-1.0",
|
|
22
|
+
"ISC",
|
|
23
|
+
"MIT",
|
|
24
|
+
"UNLICENSE",
|
|
25
|
+
}
|
|
26
|
+
REMOTE_AUDIT_FILENAME = "remote-sample-audit.json"
|
|
17
27
|
|
|
18
28
|
|
|
19
29
|
@dataclass
|
|
@@ -26,6 +36,25 @@ class RemoteSearchCandidate:
|
|
|
26
36
|
import_status: str
|
|
27
37
|
default_branch: str | None = None
|
|
28
38
|
archive_url: str | None = None
|
|
39
|
+
candidate_promotion_status: str = "reference_only"
|
|
40
|
+
imported_file_count: int = 0
|
|
41
|
+
pkt_file_count: int = 0
|
|
42
|
+
pka_file_count: int = 0
|
|
43
|
+
readme_file_count: int = 0
|
|
44
|
+
license_file_count: int = 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_permissive_license(license_or_permission: str | None) -> bool:
|
|
48
|
+
value = (license_or_permission or "").strip().upper()
|
|
49
|
+
return value in PERMISSIVE_LICENSES
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def candidate_promotion_status(license_or_permission: str | None) -> str:
|
|
53
|
+
return "validated_curated" if is_permissive_license(license_or_permission) else "reference_only"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def candidate_is_curated_eligible(candidate: RemoteSearchCandidate) -> bool:
|
|
57
|
+
return candidate.candidate_promotion_status == "validated_curated"
|
|
29
58
|
|
|
30
59
|
|
|
31
60
|
def build_remote_search_queries(plan: IntentPlan) -> list[str]:
|
|
@@ -91,6 +120,9 @@ def search_remote_candidates(
|
|
|
91
120
|
import_status="discovered",
|
|
92
121
|
default_branch=item.get("default_branch"),
|
|
93
122
|
archive_url=item.get("archive_url"),
|
|
123
|
+
candidate_promotion_status=candidate_promotion_status(
|
|
124
|
+
str((item.get("license") or {}).get("spdx_id") or "unknown")
|
|
125
|
+
),
|
|
94
126
|
)
|
|
95
127
|
)
|
|
96
128
|
if len(candidates) >= max_results:
|
|
@@ -98,19 +130,66 @@ def search_remote_candidates(
|
|
|
98
130
|
return candidates
|
|
99
131
|
|
|
100
132
|
|
|
133
|
+
def _allowed_archive_member(filename: str) -> bool:
|
|
134
|
+
lower_name = filename.lower()
|
|
135
|
+
return (
|
|
136
|
+
lower_name.endswith(".pkt")
|
|
137
|
+
or lower_name.endswith(".pka")
|
|
138
|
+
or lower_name.endswith("readme.md")
|
|
139
|
+
or lower_name.endswith("license")
|
|
140
|
+
or lower_name.endswith("license.md")
|
|
141
|
+
or lower_name.endswith("license.txt")
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _increment_file_counters(candidate: RemoteSearchCandidate, target: Path) -> None:
|
|
146
|
+
lower_name = target.name.lower()
|
|
147
|
+
candidate.imported_file_count += 1
|
|
148
|
+
if lower_name.endswith(".pkt"):
|
|
149
|
+
candidate.pkt_file_count += 1
|
|
150
|
+
elif lower_name.endswith(".pka"):
|
|
151
|
+
candidate.pka_file_count += 1
|
|
152
|
+
elif lower_name == "readme.md":
|
|
153
|
+
candidate.readme_file_count += 1
|
|
154
|
+
elif lower_name in {"license", "license.md", "license.txt"}:
|
|
155
|
+
candidate.license_file_count += 1
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _clear_import_destination(dest_root: Path) -> None:
|
|
159
|
+
if not dest_root.exists():
|
|
160
|
+
return
|
|
161
|
+
for child in dest_root.iterdir():
|
|
162
|
+
if child.is_dir():
|
|
163
|
+
for nested in child.rglob("*"):
|
|
164
|
+
if nested.is_file():
|
|
165
|
+
nested.unlink()
|
|
166
|
+
for nested_dir in sorted([item for item in child.rglob("*") if item.is_dir()], reverse=True):
|
|
167
|
+
nested_dir.rmdir()
|
|
168
|
+
child.rmdir()
|
|
169
|
+
else:
|
|
170
|
+
child.unlink()
|
|
171
|
+
|
|
172
|
+
|
|
101
173
|
def auto_import_remote_candidates(
|
|
102
174
|
candidates: list[RemoteSearchCandidate],
|
|
103
175
|
import_cache_root: Path,
|
|
104
176
|
*,
|
|
105
177
|
max_results: int = 3,
|
|
178
|
+
dry_run: bool = False,
|
|
106
179
|
) -> list[RemoteSearchCandidate]:
|
|
107
180
|
imported: list[RemoteSearchCandidate] = []
|
|
108
|
-
|
|
181
|
+
if not dry_run:
|
|
182
|
+
import_cache_root.mkdir(parents=True, exist_ok=True)
|
|
109
183
|
for candidate in candidates[:max_results]:
|
|
184
|
+
candidate.candidate_promotion_status = candidate_promotion_status(candidate.license_or_permission)
|
|
110
185
|
repo_name = candidate.repo_url.rstrip("/").split("/")[-1]
|
|
111
186
|
owner_name = candidate.repo_url.rstrip("/").split("/")[-2] if "/" in candidate.repo_url.rstrip("/") else "repo"
|
|
112
187
|
dest_root = import_cache_root / f"{owner_name}_{repo_name}"
|
|
113
188
|
archive_url = candidate.archive_url
|
|
189
|
+
if dry_run:
|
|
190
|
+
candidate.import_status = "dry_run"
|
|
191
|
+
imported.append(candidate)
|
|
192
|
+
continue
|
|
114
193
|
if not archive_url:
|
|
115
194
|
imported.append(candidate)
|
|
116
195
|
continue
|
|
@@ -124,39 +203,26 @@ def auto_import_remote_candidates(
|
|
|
124
203
|
continue
|
|
125
204
|
try:
|
|
126
205
|
with zipfile.ZipFile(io.BytesIO(archive_bytes)) as archive:
|
|
127
|
-
|
|
128
|
-
for child in dest_root.iterdir():
|
|
129
|
-
if child.is_dir():
|
|
130
|
-
for nested in child.rglob("*"):
|
|
131
|
-
if nested.is_file():
|
|
132
|
-
nested.unlink()
|
|
133
|
-
for nested_dir in sorted([item for item in child.rglob("*") if item.is_dir()], reverse=True):
|
|
134
|
-
nested_dir.rmdir()
|
|
135
|
-
child.rmdir()
|
|
136
|
-
else:
|
|
137
|
-
child.unlink()
|
|
206
|
+
_clear_import_destination(dest_root)
|
|
138
207
|
dest_root.mkdir(parents=True, exist_ok=True)
|
|
139
208
|
for member in archive.infolist():
|
|
140
|
-
|
|
141
|
-
if not (
|
|
142
|
-
lower_name.endswith(".pkt")
|
|
143
|
-
or lower_name.endswith(".pka")
|
|
144
|
-
or lower_name.endswith("readme.md")
|
|
145
|
-
or lower_name.endswith("license")
|
|
146
|
-
or lower_name.endswith("license.md")
|
|
147
|
-
or lower_name.endswith("license.txt")
|
|
148
|
-
):
|
|
209
|
+
if not _allowed_archive_member(member.filename):
|
|
149
210
|
continue
|
|
150
211
|
cleaned = re.sub(r"^[^/]+/", "", member.filename).strip("/")
|
|
151
212
|
if not cleaned:
|
|
152
213
|
continue
|
|
153
214
|
target = dest_root / cleaned
|
|
215
|
+
try:
|
|
216
|
+
target.resolve().relative_to(dest_root.resolve())
|
|
217
|
+
except ValueError:
|
|
218
|
+
continue
|
|
154
219
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
155
220
|
if member.is_dir():
|
|
156
221
|
target.mkdir(parents=True, exist_ok=True)
|
|
157
222
|
continue
|
|
158
223
|
with archive.open(member) as source, target.open("wb") as sink:
|
|
159
224
|
sink.write(source.read())
|
|
225
|
+
_increment_file_counters(candidate, target)
|
|
160
226
|
candidate.path = str(dest_root)
|
|
161
227
|
candidate.import_status = "imported"
|
|
162
228
|
except zipfile.BadZipFile:
|
|
@@ -165,5 +231,115 @@ def auto_import_remote_candidates(
|
|
|
165
231
|
return imported
|
|
166
232
|
|
|
167
233
|
|
|
234
|
+
def _decode_audit_for_root(root: Path, license_or_permission: str) -> dict[str, object]:
|
|
235
|
+
pkt_paths = sorted(root.rglob("*.pkt")) if root.exists() else []
|
|
236
|
+
decode_success_count = 0
|
|
237
|
+
decode_failure_count = 0
|
|
238
|
+
detected_features: set[str] = set()
|
|
239
|
+
detected_families: set[str] = set()
|
|
240
|
+
for pkt_path in pkt_paths:
|
|
241
|
+
try:
|
|
242
|
+
from sample_catalog import summarize_pkt_descriptor
|
|
243
|
+
|
|
244
|
+
descriptor = summarize_pkt_descriptor(
|
|
245
|
+
pkt_path,
|
|
246
|
+
str(pkt_path.relative_to(root)),
|
|
247
|
+
origin="external-reference",
|
|
248
|
+
prototype_eligible=False,
|
|
249
|
+
trust_level="reference-only",
|
|
250
|
+
role="reference",
|
|
251
|
+
license_or_permission=license_or_permission,
|
|
252
|
+
promotion_status="reference_only",
|
|
253
|
+
donor_eligible=False,
|
|
254
|
+
)
|
|
255
|
+
except Exception:
|
|
256
|
+
decode_failure_count += 1
|
|
257
|
+
continue
|
|
258
|
+
decode_success_count += 1
|
|
259
|
+
detected_features.update(descriptor.capability_tags)
|
|
260
|
+
detected_features.update(descriptor.validated_edit_capabilities)
|
|
261
|
+
detected_families.update(descriptor.archetype_tags)
|
|
262
|
+
detected_families.update(descriptor.device_families)
|
|
263
|
+
detected_families.update(descriptor.topology_tags)
|
|
264
|
+
return {
|
|
265
|
+
"decode_status": "not_run" if not pkt_paths else ("ok" if decode_failure_count == 0 else "partial_or_failed"),
|
|
266
|
+
"decode_success_count": decode_success_count,
|
|
267
|
+
"decode_failure_count": decode_failure_count,
|
|
268
|
+
"detected_feature_tags": sorted(detected_features),
|
|
269
|
+
"detected_feature_families": sorted(detected_families),
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def build_remote_sample_audit(
|
|
274
|
+
candidates: list[RemoteSearchCandidate],
|
|
275
|
+
import_cache_root: Path,
|
|
276
|
+
) -> dict[str, object]:
|
|
277
|
+
entries: list[dict[str, object]] = []
|
|
278
|
+
license_counts: dict[str, int] = {}
|
|
279
|
+
for candidate in candidates:
|
|
280
|
+
license_value = candidate.license_or_permission or "unknown"
|
|
281
|
+
license_counts[license_value] = license_counts.get(license_value, 0) + 1
|
|
282
|
+
entry = asdict(candidate)
|
|
283
|
+
if candidate.path:
|
|
284
|
+
entry.update(_decode_audit_for_root(Path(candidate.path), candidate.license_or_permission))
|
|
285
|
+
else:
|
|
286
|
+
entry.update(
|
|
287
|
+
{
|
|
288
|
+
"decode_status": "not_run",
|
|
289
|
+
"decode_success_count": 0,
|
|
290
|
+
"decode_failure_count": 0,
|
|
291
|
+
"detected_feature_tags": [],
|
|
292
|
+
"detected_feature_families": [],
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
if candidate.candidate_promotion_status == "validated_curated" and int(entry.get("decode_success_count", 0)) > 0:
|
|
296
|
+
entry["validation_promotion_status"] = "validated_curated"
|
|
297
|
+
else:
|
|
298
|
+
entry["validation_promotion_status"] = "reference_only"
|
|
299
|
+
entries.append(entry)
|
|
300
|
+
return {
|
|
301
|
+
"audit_version": "1.0",
|
|
302
|
+
"cache_root": str(import_cache_root),
|
|
303
|
+
"raw_pkt_policy": "remote Packet Tracer files stay local/cache-only and are not committed or packed",
|
|
304
|
+
"candidate_count": len(candidates),
|
|
305
|
+
"imported_repo_count": sum(1 for candidate in candidates if candidate.import_status == "imported"),
|
|
306
|
+
"dry_run_repo_count": sum(1 for candidate in candidates if candidate.import_status == "dry_run"),
|
|
307
|
+
"pkt_file_count": sum(candidate.pkt_file_count for candidate in candidates),
|
|
308
|
+
"pka_file_count": sum(candidate.pka_file_count for candidate in candidates),
|
|
309
|
+
"decode_success_count": sum(int(entry.get("decode_success_count", 0)) for entry in entries),
|
|
310
|
+
"decode_failure_count": sum(int(entry.get("decode_failure_count", 0)) for entry in entries),
|
|
311
|
+
"license_counts": license_counts,
|
|
312
|
+
"promotion_status_counts": {
|
|
313
|
+
status: sum(1 for candidate in candidates if candidate.candidate_promotion_status == status)
|
|
314
|
+
for status in sorted({candidate.candidate_promotion_status for candidate in candidates})
|
|
315
|
+
},
|
|
316
|
+
"validation_promotion_status_counts": {
|
|
317
|
+
status: sum(1 for entry in entries if entry.get("validation_promotion_status") == status)
|
|
318
|
+
for status in sorted({str(entry.get("validation_promotion_status")) for entry in entries})
|
|
319
|
+
},
|
|
320
|
+
"top_gaps_filled_by_imported_samples": sorted(
|
|
321
|
+
{
|
|
322
|
+
feature
|
|
323
|
+
for entry in entries
|
|
324
|
+
for feature in entry.get("detected_feature_tags", [])
|
|
325
|
+
if isinstance(feature, str)
|
|
326
|
+
}
|
|
327
|
+
)[:25],
|
|
328
|
+
"candidates": entries,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def write_remote_sample_audit(
|
|
333
|
+
candidates: list[RemoteSearchCandidate],
|
|
334
|
+
import_cache_root: Path,
|
|
335
|
+
audit_path: Path | None = None,
|
|
336
|
+
) -> dict[str, object]:
|
|
337
|
+
payload = build_remote_sample_audit(candidates, import_cache_root)
|
|
338
|
+
target = audit_path or (import_cache_root / REMOTE_AUDIT_FILENAME)
|
|
339
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
340
|
+
target.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
341
|
+
return payload
|
|
342
|
+
|
|
343
|
+
|
|
168
344
|
def asdict_list(candidates: list[RemoteSearchCandidate]) -> list[dict[str, object]]:
|
|
169
345
|
return [asdict(candidate) for candidate in candidates]
|
|
@@ -43,23 +43,62 @@ def runtime_env_examples(host_os: str) -> list[str]:
|
|
|
43
43
|
return [
|
|
44
44
|
r"$env:PACKET_TRACER_ROOT='C:\Program Files\Cisco Packet Tracer 9.0.0'",
|
|
45
45
|
r"$env:PACKET_TRACER_COMPAT_DONOR='C:\path\to\your-working-9.0-donor.pkt'",
|
|
46
|
-
r'$env:PKT_TWOFISH_LIBRARY="
|
|
46
|
+
r'$env:PKT_TWOFISH_LIBRARY="C:\path\to\_twofish.cp314-win_amd64.pyd"',
|
|
47
|
+
r'$env:PKT_TWOFISH_SEARCH_ROOTS="C:\path\to\bridge-folder"',
|
|
47
48
|
]
|
|
48
49
|
if host_os == "macOS":
|
|
49
50
|
return [
|
|
50
51
|
"export PACKET_TRACER_ROOT='/Applications/Cisco Packet Tracer.app/Contents/Resources'",
|
|
51
52
|
"export PACKET_TRACER_COMPAT_DONOR=\"$HOME/path/to/your-working-9.0-donor.pkt\"",
|
|
52
|
-
"export PKT_TWOFISH_SEARCH_ROOTS=\"$HOME
|
|
53
|
+
"export PKT_TWOFISH_SEARCH_ROOTS=\"$HOME/path/to/bridge-folder:$HOME/pkt-bridges\"",
|
|
53
54
|
]
|
|
54
55
|
if host_os == "Linux":
|
|
55
56
|
return [
|
|
56
57
|
"export PACKET_TRACER_ROOT='/opt/pt/bin'",
|
|
57
58
|
"export PACKET_TRACER_COMPAT_DONOR=\"$HOME/path/to/your-working-9.0-donor.pkt\"",
|
|
58
|
-
"export PKT_TWOFISH_SEARCH_ROOTS=\"$HOME
|
|
59
|
+
"export PKT_TWOFISH_SEARCH_ROOTS=\"$HOME/path/to/bridge-folder:$HOME/pkt-bridges\"",
|
|
59
60
|
]
|
|
60
61
|
return []
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
def _format_operation_list(operations: list[str]) -> str:
|
|
65
|
+
return ", ".join(operations) if operations else "none"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _best_next_fix(runtime_blockers: list[str], recommended_next_steps: list[str]) -> str:
|
|
69
|
+
blocker_map = {
|
|
70
|
+
"missing_or_incompatible_donor": "Fix the donor path first so inventory/edit/generate can use a compatible 9.0 donor.",
|
|
71
|
+
"missing_twofish_bridge": "Fix the Twofish bridge next so strict decode/edit/generate can run locally.",
|
|
72
|
+
"missing_packet_tracer_root": "Fix PACKET_TRACER_ROOT so the doctor can resolve the install layout deterministically.",
|
|
73
|
+
"missing_packet_tracer_executable": "Fix the Packet Tracer install root or executable path before relying on validate_open.",
|
|
74
|
+
"windows_first_runtime": "Do not assume non-Windows strict runtime support without a custom native bridge and explicit Packet Tracer paths.",
|
|
75
|
+
"using_external_bridge_only": "Move the external bridge into the repo-local vendor path if you need a self-contained runtime claim.",
|
|
76
|
+
}
|
|
77
|
+
for blocker in runtime_blockers:
|
|
78
|
+
if blocker in blocker_map:
|
|
79
|
+
return blocker_map[blocker]
|
|
80
|
+
return recommended_next_steps[0] if recommended_next_steps else "No immediate fix is required."
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _why_it_is_blocked(runtime_blockers: list[str], bridge_resolution: str) -> str:
|
|
84
|
+
if not runtime_blockers:
|
|
85
|
+
return "No runtime blocker is currently active."
|
|
86
|
+
reasons: list[str] = []
|
|
87
|
+
if "missing_or_incompatible_donor" in runtime_blockers:
|
|
88
|
+
reasons.append("no compatible donor is resolved")
|
|
89
|
+
if "missing_twofish_bridge" in runtime_blockers:
|
|
90
|
+
reasons.append("no local Twofish bridge is resolved")
|
|
91
|
+
if "missing_packet_tracer_root" in runtime_blockers:
|
|
92
|
+
reasons.append("Packet Tracer root is not resolved")
|
|
93
|
+
if "missing_packet_tracer_executable" in runtime_blockers:
|
|
94
|
+
reasons.append("Packet Tracer executable is not resolved")
|
|
95
|
+
if "windows_first_runtime" in runtime_blockers:
|
|
96
|
+
reasons.append("strict bundled validation is still Windows-first")
|
|
97
|
+
if bridge_resolution == "external_env":
|
|
98
|
+
reasons.append("strict runtime currently relies on an external bridge override")
|
|
99
|
+
return "; ".join(reasons) + "."
|
|
100
|
+
|
|
101
|
+
|
|
63
102
|
def build_recommended_next_steps(
|
|
64
103
|
*,
|
|
65
104
|
host_os: str,
|
|
@@ -205,13 +244,43 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
205
244
|
else:
|
|
206
245
|
runtime_grade = "ready"
|
|
207
246
|
if runtime_grade == "ready":
|
|
208
|
-
doctor_summary =
|
|
247
|
+
doctor_summary = (
|
|
248
|
+
"Runtime is ready. "
|
|
249
|
+
f"Ready operations: {_format_operation_list(ready_operations)}."
|
|
250
|
+
)
|
|
251
|
+
elif bridge_resolution == "external_env" and not blocked_operations:
|
|
252
|
+
doctor_summary = (
|
|
253
|
+
"Runtime operations are ready, but packaged runtime remains partially ready because the bridge "
|
|
254
|
+
"comes from an external environment instead of the repo-local vendor path. "
|
|
255
|
+
f"Ready operations: {_format_operation_list(ready_operations)}."
|
|
256
|
+
)
|
|
209
257
|
elif runtime_grade == "partially_ready":
|
|
210
|
-
doctor_summary =
|
|
258
|
+
doctor_summary = (
|
|
259
|
+
"Runtime is partially ready. "
|
|
260
|
+
f"Ready operations: {_format_operation_list(ready_operations)}. "
|
|
261
|
+
f"Blocked operations: {_format_operation_list(blocked_operations)}."
|
|
262
|
+
)
|
|
211
263
|
else:
|
|
212
|
-
doctor_summary =
|
|
213
|
-
|
|
264
|
+
doctor_summary = (
|
|
265
|
+
"Runtime is blocked. "
|
|
266
|
+
f"Blocked operations: {_format_operation_list(blocked_operations)}."
|
|
267
|
+
)
|
|
268
|
+
if bridge_resolution == "external_env" and blocked_operations:
|
|
214
269
|
doctor_summary = f"{doctor_summary} External bridge is being used instead of a repo-local vendor bridge."
|
|
270
|
+
elif bridge_resolution == "missing" and runtime_grade != "ready":
|
|
271
|
+
doctor_summary = f"{doctor_summary} Strict decode/edit/generate remain blocked until a local bridge is resolved."
|
|
272
|
+
what_currently_works = (
|
|
273
|
+
f"Currently working operations: {_format_operation_list(ready_operations)}."
|
|
274
|
+
if ready_operations
|
|
275
|
+
else "No strict runtime operations are currently working."
|
|
276
|
+
)
|
|
277
|
+
what_is_blocked = (
|
|
278
|
+
f"Blocked operations: {_format_operation_list(blocked_operations)}."
|
|
279
|
+
if blocked_operations
|
|
280
|
+
else "No runtime operations are currently blocked."
|
|
281
|
+
)
|
|
282
|
+
why_it_is_blocked = _why_it_is_blocked(runtime_blockers, bridge_resolution)
|
|
283
|
+
best_next_fix = _best_next_fix(runtime_blockers, recommended_next_steps)
|
|
215
284
|
bridge_recommendation = (
|
|
216
285
|
"Use or install a repo-local vendor bridge for fully self-contained runtime readiness."
|
|
217
286
|
if bridge_resolution == "external_env"
|
|
@@ -220,11 +289,11 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
220
289
|
else "Repo-local bridge is resolved."
|
|
221
290
|
)
|
|
222
291
|
runtime_contract_notes = (
|
|
223
|
-
"Repo-local bridge and donor are present."
|
|
292
|
+
"Repo-local bridge and donor are present, so this checkout can run strict decode/edit/generate locally."
|
|
224
293
|
if bridge_resolution == "repo_local"
|
|
225
|
-
else "
|
|
294
|
+
else "Strict decode/edit/generate currently rely on an external bridge path. Repo-local runtime packaging is still incomplete."
|
|
226
295
|
if bridge_resolution == "external_env"
|
|
227
|
-
else "
|
|
296
|
+
else "No bridge is resolved. validate_open may still work when Packet Tracer is installed, but strict decode/edit/generate remain blocked."
|
|
228
297
|
)
|
|
229
298
|
|
|
230
299
|
return {
|
|
@@ -270,6 +339,10 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
270
339
|
"runtime_blockers": runtime_blockers,
|
|
271
340
|
"ready_operations": ready_operations,
|
|
272
341
|
"blocked_operations": blocked_operations,
|
|
342
|
+
"what_currently_works": what_currently_works,
|
|
343
|
+
"what_is_blocked": what_is_blocked,
|
|
344
|
+
"why_it_is_blocked": why_it_is_blocked,
|
|
345
|
+
"best_next_fix": best_next_fix,
|
|
273
346
|
"doctor_summary": doctor_summary,
|
|
274
347
|
"runtime_grade": runtime_grade,
|
|
275
348
|
"recommended_next_steps": recommended_next_steps,
|