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.
Files changed (66) hide show
  1. package/CHANGELOG.md +424 -42
  2. package/README.md +535 -250
  3. package/SKILL.md +337 -262
  4. package/bin/packet-tracer-skill.js +29 -2
  5. package/docs/automation-controller-proof.md +35 -0
  6. package/docs/curated-donor-registry.md +11 -0
  7. package/docs/generate-ready-pilot-design.md +30 -0
  8. package/docs/github-launch-ops-0.2.3.md +37 -0
  9. package/docs/github-metadata.md +6 -4
  10. package/docs/hero-demo-plan.md +1 -1
  11. package/docs/home-iot-donor-proof.md +4 -4
  12. package/docs/industrial-programming-proof.md +48 -0
  13. package/docs/ipv4-routing-management-proof.md +37 -0
  14. package/docs/l2-resiliency-bgp-proof.md +60 -0
  15. package/docs/l2-security-qos-proof.md +59 -0
  16. package/docs/packet-tracer-feature-gap-atlas.md +174 -17
  17. package/docs/post-launch-follow-up.md +9 -5
  18. package/docs/proof-readiness-dashboard.md +69 -0
  19. package/docs/publish-preview-roadmap.md +6 -5
  20. package/docs/release-checklist.md +27 -13
  21. package/docs/release-notes-0.2.2.md +1 -1
  22. package/docs/release-notes-0.2.3.md +59 -0
  23. package/docs/release-notes-0.2.4.md +20 -0
  24. package/docs/runtime-truth.md +33 -8
  25. package/docs/security-edge-deepening-proof.md +65 -0
  26. package/docs/voice-collaboration-proof.md +38 -0
  27. package/docs/wan-security-donor-proof.md +20 -3
  28. package/examples/README.md +98 -69
  29. package/examples/complex_campus_master_edit_v4.inventory.json +12 -2
  30. package/examples/gallery.md +94 -6
  31. package/examples/home_iot_cli_edit_v1.inventory.json +11 -2
  32. package/examples/index.json +932 -4
  33. package/examples/local-sample-evidence.json +24 -0
  34. package/examples/proof-cards.json +117 -0
  35. package/examples/service_heavy_cli_edit_v1.inventory.json +11 -2
  36. package/package.json +60 -44
  37. package/pytest.ini +9 -0
  38. package/references/packettracer-feature-atlas.json +67 -17
  39. package/references/packettracer-sample-catalog.json +45287 -4525
  40. package/references/packettracer-sample-catalog.md +599 -259
  41. package/references/proof-readiness-candidates.json +352 -0
  42. package/scripts/build_examples_index.py +228 -35
  43. package/scripts/build_sample_catalog.py +24 -44
  44. package/scripts/corpus_runner.py +430 -0
  45. package/scripts/coverage_matrix.py +1842 -1319
  46. package/scripts/donor_cache.py +354 -0
  47. package/scripts/donor_diagnostics.py +3 -1
  48. package/scripts/feature_atlas.py +65 -1
  49. package/scripts/generate_pkt.py +8762 -4070
  50. package/scripts/intent_parser.py +2242 -1138
  51. package/scripts/local_donors.py +340 -0
  52. package/scripts/packet_tracer_env.py +846 -391
  53. package/scripts/pkt_annotate.py +218 -0
  54. package/scripts/pkt_codec.py +420 -181
  55. package/scripts/pkt_editor.py +2405 -1226
  56. package/scripts/pkt_transformer.py +1072 -727
  57. package/scripts/pkt_verify.py +461 -0
  58. package/scripts/remote_search.py +197 -21
  59. package/scripts/runtime_doctor.py +80 -29
  60. package/scripts/sample_catalog.py +1372 -1195
  61. package/scripts/twofish_diagnostics.py +48 -31
  62. package/scripts/usage_ledger.py +218 -0
  63. package/scripts/vendor/README.md +44 -37
  64. package/scripts/vendor/twofish_pure.py +321 -0
  65. package/scripts/workspace_repair.py +548 -508
  66. 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 _detect_screenshots(example_name: str) -> list[str]:
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-generate"
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=generate-ready, iot_registration=donor-backed constrained-generate, wireless_ap=generate-ready"
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}=generate-ready" for cap in primary_capabilities[:3]) if primary_capabilities else "no parity excerpt"
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
- "name": name,
219
- "title": TITLE_BY_FAMILY.get(family, name.replace("_", " ").title()),
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
- "inventory_json": manifest_path.relative_to(ROOT).as_posix(),
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
- return {
245
- "curated_examples": [
246
- _build_entry(path)
247
- for path in _manifest_files()
248
- ]
249
- }
250
-
251
-
252
- def build_examples_gallery_markdown(payload: dict[str, object]) -> str:
253
- lines = [
254
- "## Known Working Scenario Set",
255
- "",
256
- "These examples are public, text-first proof artifacts derived from donor-backed workflows and aligned with the scenario fixture corpus.",
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.1` canonical public set:",
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
- missing = [entry for entry in payload["curated_examples"] if not entry.get("screenshots")]
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
- from __future__ import annotations
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
- from packet_tracer_env import require_packet_tracer_saves_root
7
- from pkt_codec import decode_pkt_modern
8
- from sample_catalog import write_catalog_outputs
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
- def summarize_pkt(path: Path) -> dict:
12
- saves_root = require_packet_tracer_saves_root()
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(summarize_pkt(path))
24
+ items.append(_summarize_pkt(path, relative_path, "cisco-local", True))
52
25
  except Exception as exc:
53
- items.append({"relative_path": str(path.relative_to(saves_root)), "error": f"{type(exc).__name__}: {exc}"})
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
- print(f"Wrote {len(items)} sample entries")
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__":