packet-tracer-skill 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +424 -42
- package/README.md +535 -250
- package/SKILL.md +337 -262
- package/bin/packet-tracer-skill.js +29 -2
- package/docs/automation-controller-proof.md +35 -0
- package/docs/curated-donor-registry.md +11 -0
- package/docs/generate-ready-pilot-design.md +30 -0
- package/docs/github-launch-ops-0.2.3.md +37 -0
- package/docs/github-metadata.md +6 -4
- package/docs/hero-demo-plan.md +1 -1
- package/docs/home-iot-donor-proof.md +4 -4
- 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/packet-tracer-feature-gap-atlas.md +174 -17
- package/docs/post-launch-follow-up.md +9 -5
- package/docs/proof-readiness-dashboard.md +69 -0
- package/docs/publish-preview-roadmap.md +6 -5
- package/docs/release-checklist.md +27 -13
- package/docs/release-notes-0.2.2.md +1 -1
- package/docs/release-notes-0.2.3.md +59 -0
- package/docs/release-notes-0.2.4.md +20 -0
- package/docs/runtime-truth.md +33 -8
- package/docs/security-edge-deepening-proof.md +65 -0
- package/docs/voice-collaboration-proof.md +38 -0
- package/docs/wan-security-donor-proof.md +20 -3
- package/examples/README.md +98 -69
- package/examples/complex_campus_master_edit_v4.inventory.json +12 -2
- package/examples/gallery.md +94 -6
- package/examples/home_iot_cli_edit_v1.inventory.json +11 -2
- package/examples/index.json +932 -4
- package/examples/local-sample-evidence.json +24 -0
- package/examples/proof-cards.json +117 -0
- package/examples/service_heavy_cli_edit_v1.inventory.json +11 -2
- package/package.json +60 -44
- package/pytest.ini +9 -0
- package/references/packettracer-feature-atlas.json +67 -17
- package/references/packettracer-sample-catalog.json +45287 -4525
- package/references/packettracer-sample-catalog.md +599 -259
- package/references/proof-readiness-candidates.json +352 -0
- package/scripts/build_examples_index.py +228 -35
- package/scripts/build_sample_catalog.py +24 -44
- package/scripts/corpus_runner.py +430 -0
- package/scripts/coverage_matrix.py +1842 -1319
- package/scripts/donor_cache.py +354 -0
- package/scripts/donor_diagnostics.py +3 -1
- package/scripts/feature_atlas.py +65 -1
- package/scripts/generate_pkt.py +8762 -4070
- package/scripts/intent_parser.py +2242 -1138
- package/scripts/local_donors.py +340 -0
- package/scripts/packet_tracer_env.py +846 -391
- package/scripts/pkt_annotate.py +218 -0
- package/scripts/pkt_codec.py +420 -181
- package/scripts/pkt_editor.py +2405 -1226
- package/scripts/pkt_transformer.py +1072 -727
- package/scripts/pkt_verify.py +461 -0
- package/scripts/remote_search.py +197 -21
- package/scripts/runtime_doctor.py +80 -29
- package/scripts/sample_catalog.py +1372 -1195
- package/scripts/twofish_diagnostics.py +48 -31
- package/scripts/usage_ledger.py +218 -0
- package/scripts/vendor/README.md +44 -37
- package/scripts/vendor/twofish_pure.py +321 -0
- package/scripts/workspace_repair.py +548 -508
- package/templates/pt900/donors/README.md +15 -0
|
@@ -8,8 +8,12 @@ ROOT = Path(__file__).resolve().parents[1]
|
|
|
8
8
|
EXAMPLES_DIR = ROOT / "examples"
|
|
9
9
|
SCREENSHOTS_DIR = EXAMPLES_DIR / "screenshots"
|
|
10
10
|
PREVIEWS_DIR = EXAMPLES_DIR / "previews"
|
|
11
|
-
INDEX_PATH = EXAMPLES_DIR / "index.json"
|
|
12
|
-
GALLERY_PATH = EXAMPLES_DIR / "gallery.md"
|
|
11
|
+
INDEX_PATH = EXAMPLES_DIR / "index.json"
|
|
12
|
+
GALLERY_PATH = EXAMPLES_DIR / "gallery.md"
|
|
13
|
+
PROOF_CARDS_PATH = EXAMPLES_DIR / "proof-cards.json"
|
|
14
|
+
LOCAL_SAMPLE_EVIDENCE_PATH = EXAMPLES_DIR / "local-sample-evidence.json"
|
|
15
|
+
LOCAL_SAMPLE_AUDIT_PATH = ROOT / "output" / "local-sample-audit.json"
|
|
16
|
+
PROOF_READINESS_CANDIDATES_PATH = ROOT / "references" / "proof-readiness-candidates.json"
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
TITLE_BY_FAMILY = {
|
|
@@ -74,15 +78,105 @@ CARD_THEME_BY_FAMILY = {
|
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
|
|
77
|
-
def _manifest_files() -> list[Path]:
|
|
81
|
+
def _manifest_files() -> list[Path]:
|
|
78
82
|
return sorted(
|
|
79
83
|
path
|
|
80
84
|
for path in EXAMPLES_DIR.glob("*.inventory.json")
|
|
81
85
|
if path.is_file()
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _load_proof_cards() -> list[dict[str, object]]:
|
|
90
|
+
if not PROOF_CARDS_PATH.exists():
|
|
91
|
+
return []
|
|
92
|
+
payload = json.loads(PROOF_CARDS_PATH.read_text(encoding="utf-8"))
|
|
93
|
+
cards = payload.get("proof_cards", []) if isinstance(payload, dict) else []
|
|
94
|
+
normalized: list[dict[str, object]] = []
|
|
95
|
+
for card in cards:
|
|
96
|
+
if not isinstance(card, dict):
|
|
97
|
+
continue
|
|
98
|
+
entry = dict(card)
|
|
99
|
+
entry.setdefault("schema_version", "examples.truth.v2")
|
|
100
|
+
entry.setdefault("artifact_type", "proof_card")
|
|
101
|
+
entry.setdefault("artifact_policy", {
|
|
102
|
+
"commit_pkt_binary": False,
|
|
103
|
+
"commit_inventory_json": False,
|
|
104
|
+
"commit_screenshots": False,
|
|
105
|
+
"raw_source_public": False,
|
|
106
|
+
})
|
|
107
|
+
entry.setdefault("try_command", entry.get("explicit_command", ""))
|
|
108
|
+
entry.setdefault("does_not_claim", entry.get("refusal_boundary", "No broad generate-ready support is claimed by this proof card."))
|
|
109
|
+
entry.setdefault(
|
|
110
|
+
"support_level_explanation",
|
|
111
|
+
f"{entry.get('support_level', 'report_supported')} is a proof-card support level, not broad topology generation.",
|
|
112
|
+
)
|
|
113
|
+
entry.setdefault("maturity_summary", {
|
|
114
|
+
"atlas_status": entry.get("support_level", "report_supported"),
|
|
115
|
+
"example_status": "proof_card",
|
|
116
|
+
"donor_backed_ready": entry.get("support_level") == "donor_backed_ready",
|
|
117
|
+
"generate_ready": False,
|
|
118
|
+
})
|
|
119
|
+
normalized.append(entry)
|
|
120
|
+
return normalized
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _load_local_sample_evidence() -> dict[str, object]:
|
|
124
|
+
if LOCAL_SAMPLE_EVIDENCE_PATH.exists():
|
|
125
|
+
payload = json.loads(LOCAL_SAMPLE_EVIDENCE_PATH.read_text(encoding="utf-8"))
|
|
126
|
+
payload.setdefault("source", "examples/local-sample-evidence.json")
|
|
127
|
+
payload.setdefault("available", True)
|
|
128
|
+
return payload
|
|
129
|
+
if not LOCAL_SAMPLE_AUDIT_PATH.exists():
|
|
130
|
+
return {
|
|
131
|
+
"source": "output/local-sample-audit.json",
|
|
132
|
+
"available": False,
|
|
133
|
+
"summary": "No local sample audit artifact was found.",
|
|
134
|
+
"top_capabilities": [],
|
|
135
|
+
}
|
|
136
|
+
payload = json.loads(LOCAL_SAMPLE_AUDIT_PATH.read_text(encoding="utf-8"))
|
|
137
|
+
capabilities = payload.get("detected_config_capabilities", {})
|
|
138
|
+
top_capabilities = [
|
|
139
|
+
{
|
|
140
|
+
"capability": name,
|
|
141
|
+
"sample_count": details.get("sample_count", 0),
|
|
142
|
+
"examples": details.get("examples", [])[:3],
|
|
143
|
+
}
|
|
144
|
+
for name, details in sorted(
|
|
145
|
+
capabilities.items(),
|
|
146
|
+
key=lambda item: item[1].get("sample_count", 0),
|
|
147
|
+
reverse=True,
|
|
148
|
+
)[:12]
|
|
149
|
+
if isinstance(details, dict)
|
|
150
|
+
]
|
|
151
|
+
return {
|
|
152
|
+
"source": "output/local-sample-audit.json",
|
|
153
|
+
"available": True,
|
|
154
|
+
"policy": "Local user-supplied samples are evidence inputs only, not curated donor truth or npm package content.",
|
|
155
|
+
"total_files": payload.get("total_files"),
|
|
156
|
+
"decode_success_count": payload.get("decode_success_count"),
|
|
157
|
+
"decode_fail_count": payload.get("decode_fail_count"),
|
|
158
|
+
"top_capabilities": top_capabilities,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _load_proof_readiness_candidates() -> dict[str, object]:
|
|
163
|
+
if not PROOF_READINESS_CANDIDATES_PATH.exists():
|
|
164
|
+
return {
|
|
165
|
+
"schema_version": "proof_readiness.v1",
|
|
166
|
+
"available": False,
|
|
167
|
+
"summary": "No proof-readiness candidate artifact was found.",
|
|
168
|
+
"primary_candidates": [],
|
|
169
|
+
"secondary_candidates": [],
|
|
170
|
+
}
|
|
171
|
+
payload = json.loads(PROOF_READINESS_CANDIDATES_PATH.read_text(encoding="utf-8"))
|
|
172
|
+
payload.setdefault("schema_version", "proof_readiness.v1")
|
|
173
|
+
payload.setdefault("available", True)
|
|
174
|
+
payload.setdefault("primary_candidates", [])
|
|
175
|
+
payload.setdefault("secondary_candidates", [])
|
|
176
|
+
return payload
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _detect_screenshots(example_name: str) -> list[str]:
|
|
86
180
|
screenshots: list[Path] = []
|
|
87
181
|
primary = SCREENSHOTS_DIR / f"{example_name}.png"
|
|
88
182
|
if primary.exists():
|
|
@@ -189,7 +283,7 @@ def _write_preview(manifest_path: Path) -> str:
|
|
|
189
283
|
return path.relative_to(ROOT).as_posix()
|
|
190
284
|
|
|
191
285
|
|
|
192
|
-
def _build_entry(manifest_path: Path) -> dict[str, object]:
|
|
286
|
+
def _build_entry(manifest_path: Path) -> dict[str, object]:
|
|
193
287
|
payload = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
194
288
|
name = str(payload["example_name"])
|
|
195
289
|
family = str(payload.get("scenario_family") or payload.get("topology_summary", {}).get("network_style") or "general")
|
|
@@ -205,18 +299,20 @@ def _build_entry(manifest_path: Path) -> dict[str, object]:
|
|
|
205
299
|
primary_capabilities = CAPABILITIES_BY_FAMILY.get(family, [])
|
|
206
300
|
acceptance_excerpt = f"{acceptance_label} | donor={donor_origin} | capabilities={', '.join(primary_capabilities[:3])}"
|
|
207
301
|
if family == "home_iot":
|
|
208
|
-
acceptance_excerpt += " | mode=donor-backed constrained
|
|
302
|
+
acceptance_excerpt += " | mode=donor-backed constrained edit"
|
|
209
303
|
fixture_name = FIXTURE_BY_FAMILY.get(family)
|
|
210
304
|
matrix_excerpt = f"{fixture_name or 'ad-hoc'} | {acceptance_label} | family={family}"
|
|
211
305
|
if family == "home_iot":
|
|
212
|
-
parity_excerpt = "iot=
|
|
306
|
+
parity_excerpt = "iot=known_working_example, iot_registration=donor_backed_ready, wireless_ap=known_working_example"
|
|
213
307
|
else:
|
|
214
|
-
parity_excerpt = ", ".join(f"{cap}=
|
|
308
|
+
parity_excerpt = ", ".join(f"{cap}=known_working_example" for cap in primary_capabilities[:3]) if primary_capabilities else "no parity excerpt"
|
|
215
309
|
decision_excerpt = f"decision={acceptance_label} | donor_origin={donor_origin}"
|
|
216
310
|
runtime_excerpt = "runtime=donor-backed example artifact"
|
|
217
|
-
return {
|
|
218
|
-
"
|
|
219
|
-
"
|
|
311
|
+
return {
|
|
312
|
+
"schema_version": str(payload.get("schema_version") or "examples.truth.v2"),
|
|
313
|
+
"artifact_type": "showcase_example",
|
|
314
|
+
"name": name,
|
|
315
|
+
"title": TITLE_BY_FAMILY.get(family, name.replace("_", " ").title()),
|
|
220
316
|
"scenario_family": family,
|
|
221
317
|
"summary": SUMMARY_BY_FAMILY.get(family, f"Curated {family} example manifest."),
|
|
222
318
|
"capabilities": primary_capabilities,
|
|
@@ -229,8 +325,20 @@ def _build_entry(manifest_path: Path) -> dict[str, object]:
|
|
|
229
325
|
"matrix_excerpt": matrix_excerpt,
|
|
230
326
|
"parity_excerpt": parity_excerpt,
|
|
231
327
|
"decision_excerpt": decision_excerpt,
|
|
232
|
-
"runtime_excerpt": runtime_excerpt,
|
|
233
|
-
"
|
|
328
|
+
"runtime_excerpt": runtime_excerpt,
|
|
329
|
+
"artifact_policy": payload.get("artifact_policy") or {
|
|
330
|
+
"commit_pkt_binary": False,
|
|
331
|
+
"commit_inventory_json": True,
|
|
332
|
+
"commit_screenshots": bool(screenshots),
|
|
333
|
+
"raw_source_public": False,
|
|
334
|
+
},
|
|
335
|
+
"maturity_summary": payload.get("maturity_summary") or {
|
|
336
|
+
"atlas_status": "known_working_example",
|
|
337
|
+
"example_status": acceptance_label,
|
|
338
|
+
"donor_backed_ready": donor_origin == "donor-backed",
|
|
339
|
+
"generate_ready": False,
|
|
340
|
+
},
|
|
341
|
+
"inventory_json": manifest_path.relative_to(ROOT).as_posix(),
|
|
234
342
|
"screenshots": screenshots,
|
|
235
343
|
"screenshot_count": len(screenshots),
|
|
236
344
|
"screenshot": screenshot,
|
|
@@ -240,22 +348,34 @@ def _build_entry(manifest_path: Path) -> dict[str, object]:
|
|
|
240
348
|
}
|
|
241
349
|
|
|
242
350
|
|
|
243
|
-
def build_examples_index() -> dict[str, object]:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
"
|
|
351
|
+
def build_examples_index() -> dict[str, object]:
|
|
352
|
+
showcase_examples = [
|
|
353
|
+
_build_entry(path)
|
|
354
|
+
for path in _manifest_files()
|
|
355
|
+
]
|
|
356
|
+
proof_cards = _load_proof_cards()
|
|
357
|
+
return {
|
|
358
|
+
"schema_version": "examples.truth.v2",
|
|
359
|
+
"release_line": "0.2.4 candidate proof surface",
|
|
360
|
+
"support_truth": {
|
|
361
|
+
"generate_ready": 0,
|
|
362
|
+
"policy": "Example artifacts can be known-working, edit-proven, or donor-backed-ready without making broad generation ready.",
|
|
363
|
+
},
|
|
364
|
+
"showcase_examples": showcase_examples,
|
|
365
|
+
"proof_cards": proof_cards,
|
|
366
|
+
"local_sample_evidence": _load_local_sample_evidence(),
|
|
367
|
+
"proof_readiness_candidates": _load_proof_readiness_candidates(),
|
|
368
|
+
"curated_examples": showcase_examples,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def build_examples_gallery_markdown(payload: dict[str, object]) -> str:
|
|
373
|
+
lines = [
|
|
374
|
+
"## Showcase Examples",
|
|
375
|
+
"",
|
|
376
|
+
"These examples are public, text-first proof artifacts derived from donor-backed workflows and aligned with the scenario fixture corpus.",
|
|
257
377
|
"",
|
|
258
|
-
"`0.2.
|
|
378
|
+
"`0.2.4` candidate examples surface, built on the published `0.2.3` capability release:",
|
|
259
379
|
"",
|
|
260
380
|
"- `campus`",
|
|
261
381
|
"- `home_iot`",
|
|
@@ -271,6 +391,12 @@ def build_examples_gallery_markdown(payload: dict[str, object]) -> str:
|
|
|
271
391
|
"- [home IoT donor proof](../docs/home-iot-donor-proof.md)",
|
|
272
392
|
"- [WAN/security donor proof](../docs/wan-security-donor-proof.md)",
|
|
273
393
|
"",
|
|
394
|
+
"Support truth:",
|
|
395
|
+
"",
|
|
396
|
+
"- showcase examples are screenshot + inventory artifacts for known working donor-backed workflows",
|
|
397
|
+
"- proof cards are text-only evidence for explicit edit paths and donor-backed readiness",
|
|
398
|
+
"- atlas `generate_ready=0` remains intentional; these examples do not claim broad generation support",
|
|
399
|
+
"",
|
|
274
400
|
"| Title | Family | Capabilities | Image | Inventory |",
|
|
275
401
|
"| --- | --- | --- | --- | --- |",
|
|
276
402
|
]
|
|
@@ -295,10 +421,77 @@ def build_examples_gallery_markdown(payload: dict[str, object]) -> str:
|
|
|
295
421
|
f"[detail {index}]({Path(path).relative_to('examples').as_posix()})"
|
|
296
422
|
for index, path in enumerate(entry.get("detail_images") or [], start=1)
|
|
297
423
|
]
|
|
298
|
-
if detail_images:
|
|
299
|
-
lines.append(f"| | | extra visuals: {'; '.join(detail_images)} | | |")
|
|
300
|
-
|
|
301
|
-
|
|
424
|
+
if detail_images:
|
|
425
|
+
lines.append(f"| | | extra visuals: {'; '.join(detail_images)} | | |")
|
|
426
|
+
|
|
427
|
+
proof_cards = payload.get("proof_cards") or []
|
|
428
|
+
if proof_cards:
|
|
429
|
+
lines.extend(
|
|
430
|
+
[
|
|
431
|
+
"",
|
|
432
|
+
"## 0.2.4 Candidate Proof Cards",
|
|
433
|
+
"",
|
|
434
|
+
"| Title | Family | Support | Proof | Boundary |",
|
|
435
|
+
"| --- | --- | --- | --- | --- |",
|
|
436
|
+
]
|
|
437
|
+
)
|
|
438
|
+
for card in proof_cards:
|
|
439
|
+
proof_doc = str(card.get("proof_doc", ""))
|
|
440
|
+
proof_href = f"../{proof_doc}" if proof_doc.startswith("docs/") else proof_doc
|
|
441
|
+
proof_link = f"[proof]({proof_href})"
|
|
442
|
+
lines.append(
|
|
443
|
+
f"| {card['title']} | `{card['scenario_family']}` | `{card['support_level']}` | {proof_link} | {card['refusal_boundary']} |"
|
|
444
|
+
)
|
|
445
|
+
lines.append(f"| | | `{card['parity_excerpt']}` | | |")
|
|
446
|
+
lines.append(f"| | | try this command: `{card['try_command']}` | | |")
|
|
447
|
+
lines.append(f"| | | does not claim: {card['does_not_claim']} | | |")
|
|
448
|
+
|
|
449
|
+
local_evidence = payload.get("local_sample_evidence") or {}
|
|
450
|
+
if local_evidence.get("available"):
|
|
451
|
+
lines.extend(
|
|
452
|
+
[
|
|
453
|
+
"",
|
|
454
|
+
"## Local Sample Evidence Board",
|
|
455
|
+
"",
|
|
456
|
+
f"Local audit source: `{local_evidence['source']}`.",
|
|
457
|
+
"",
|
|
458
|
+
f"Audit summary: `{local_evidence.get('total_files')}` files, `{local_evidence.get('decode_success_count')}` decode successes, `{local_evidence.get('decode_fail_count')}` decode failures.",
|
|
459
|
+
"",
|
|
460
|
+
"This is local evidence only. It does not make user-supplied `.pkt/.pka` files public curated donors, and it does not enter the npm package.",
|
|
461
|
+
"",
|
|
462
|
+
"| Capability | Sample Count | Example Paths |",
|
|
463
|
+
"| --- | --- | --- |",
|
|
464
|
+
]
|
|
465
|
+
)
|
|
466
|
+
for item in local_evidence.get("top_capabilities", []):
|
|
467
|
+
examples = "; ".join(item.get("examples", []))
|
|
468
|
+
lines.append(f"| `{item['capability']}` | {item['sample_count']} | {examples} |")
|
|
469
|
+
|
|
470
|
+
readiness = payload.get("proof_readiness_candidates") or {}
|
|
471
|
+
if readiness.get("available"):
|
|
472
|
+
lines.extend(
|
|
473
|
+
[
|
|
474
|
+
"",
|
|
475
|
+
"## Proof-Readiness Promotion Queue",
|
|
476
|
+
"",
|
|
477
|
+
"This queue connects proof cards, feature atlas status, and local sample evidence. It is a planning artifact, not a `generate_ready` claim.",
|
|
478
|
+
"",
|
|
479
|
+
f"Dashboard: [proof-readiness dashboard](../{readiness.get('dashboard_doc', 'docs/proof-readiness-dashboard.md')})",
|
|
480
|
+
"",
|
|
481
|
+
"| Priority | Capability | Family | Current Status | Explicit Command | Next Safe Action | Blocker |",
|
|
482
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
483
|
+
]
|
|
484
|
+
)
|
|
485
|
+
for priority, candidates in (
|
|
486
|
+
("primary", readiness.get("primary_candidates", [])),
|
|
487
|
+
("secondary", readiness.get("secondary_candidates", [])),
|
|
488
|
+
):
|
|
489
|
+
for candidate in candidates:
|
|
490
|
+
lines.append(
|
|
491
|
+
f"| `{priority}` | `{candidate['capability']}` | `{candidate['scenario_family']}` | `{candidate['current_status']}` | `{candidate.get('explicit_command', '')}` | {candidate['next_safe_action']} | `{candidate['promotion_blocker']}` |"
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
missing = [entry for entry in payload["curated_examples"] if not entry.get("screenshots")]
|
|
302
495
|
if missing:
|
|
303
496
|
lines.extend(
|
|
304
497
|
[
|
|
@@ -1,56 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
import xml.etree.ElementTree as ET
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Rebuild the installed-sample catalogue from the local Packet Tracer saves.
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
from
|
|
4
|
+
This used to carry its own `summarize_pkt`, which omitted link endpoints. The
|
|
5
|
+
committed catalogue was therefore written with 1051 link records that had no
|
|
6
|
+
`from`/`to`, and every downstream check that reasoned about donor topology was
|
|
7
|
+
comparing empty strings against each other. `sample_catalog._summarize_pkt`
|
|
8
|
+
extracts endpoints correctly and reads both container variants, so the builder
|
|
9
|
+
now calls it rather than keeping a second, weaker copy.
|
|
10
|
+
"""
|
|
9
11
|
|
|
12
|
+
from __future__ import annotations
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
xml = decode_pkt_modern(path.read_bytes())
|
|
14
|
-
root = ET.fromstring(xml)
|
|
15
|
-
devices = []
|
|
16
|
-
for device in root.findall(".//DEVICES/DEVICE"):
|
|
17
|
-
type_node = device.find("./ENGINE/TYPE")
|
|
18
|
-
devices.append(
|
|
19
|
-
{
|
|
20
|
-
"name": device.findtext("./ENGINE/NAME", default=""),
|
|
21
|
-
"type": device.findtext("./ENGINE/TYPE", default=""),
|
|
22
|
-
"model": type_node.get("model", "") if type_node is not None else "",
|
|
23
|
-
}
|
|
24
|
-
)
|
|
25
|
-
links = []
|
|
26
|
-
for link in root.findall(".//LINKS/LINK"):
|
|
27
|
-
cable = link.find("./CABLE")
|
|
28
|
-
ports = cable.findall("PORT") if cable is not None else []
|
|
29
|
-
links.append(
|
|
30
|
-
{
|
|
31
|
-
"type": link.findtext("./TYPE", default=""),
|
|
32
|
-
"cable_type": cable.findtext("./TYPE", default="") if cable is not None else "",
|
|
33
|
-
"ports": [port.text or "" for port in ports[:2]],
|
|
34
|
-
}
|
|
35
|
-
)
|
|
36
|
-
return {
|
|
37
|
-
"relative_path": str(path.relative_to(saves_root)),
|
|
38
|
-
"version": root.findtext("./VERSION", default=""),
|
|
39
|
-
"device_count": len(devices),
|
|
40
|
-
"link_count": len(links),
|
|
41
|
-
"devices": devices,
|
|
42
|
-
"links": links,
|
|
43
|
-
}
|
|
14
|
+
from packet_tracer_env import require_packet_tracer_saves_root
|
|
15
|
+
from sample_catalog import _summarize_pkt, write_catalog_outputs
|
|
44
16
|
|
|
45
17
|
|
|
46
18
|
def build_catalog() -> list[dict]:
|
|
47
19
|
saves_root = require_packet_tracer_saves_root()
|
|
48
|
-
items = []
|
|
20
|
+
items: list[dict] = []
|
|
49
21
|
for path in sorted(saves_root.rglob("*.pkt")):
|
|
22
|
+
relative_path = str(path.relative_to(saves_root))
|
|
50
23
|
try:
|
|
51
|
-
items.append(
|
|
24
|
+
items.append(_summarize_pkt(path, relative_path, "cisco-local", True))
|
|
52
25
|
except Exception as exc:
|
|
53
|
-
items.append({"relative_path":
|
|
26
|
+
items.append({"relative_path": relative_path, "error": f"{type(exc).__name__}: {exc}"})
|
|
54
27
|
return items
|
|
55
28
|
|
|
56
29
|
|
|
@@ -58,7 +31,14 @@ def main() -> None:
|
|
|
58
31
|
require_packet_tracer_saves_root()
|
|
59
32
|
items = build_catalog()
|
|
60
33
|
write_catalog_outputs(items)
|
|
61
|
-
|
|
34
|
+
readable = sum(1 for item in items if "error" not in item)
|
|
35
|
+
with_endpoints = sum(
|
|
36
|
+
1
|
|
37
|
+
for item in items
|
|
38
|
+
for link in item.get("links", [])
|
|
39
|
+
if str(link.get("from") or "") and str(link.get("to") or "")
|
|
40
|
+
)
|
|
41
|
+
print(f"Wrote {len(items)} entries ({readable} readable, {with_endpoints} links with endpoints)")
|
|
62
42
|
|
|
63
43
|
|
|
64
44
|
if __name__ == "__main__":
|