packet-tracer-skill 0.2.3 → 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 (53) hide show
  1. package/CHANGELOG.md +424 -73
  2. package/README.md +557 -442
  3. package/SKILL.md +337 -262
  4. package/bin/packet-tracer-skill.js +29 -2
  5. package/docs/github-launch-ops-0.2.3.md +37 -0
  6. package/docs/github-metadata.md +6 -4
  7. package/docs/hero-demo-plan.md +1 -1
  8. package/docs/home-iot-donor-proof.md +4 -4
  9. package/docs/l2-security-qos-proof.md +1 -1
  10. package/docs/packet-tracer-feature-gap-atlas.md +4 -4
  11. package/docs/post-launch-follow-up.md +9 -5
  12. package/docs/proof-readiness-dashboard.md +69 -0
  13. package/docs/publish-preview-roadmap.md +6 -5
  14. package/docs/release-checklist.md +17 -8
  15. package/docs/release-notes-0.2.4.md +20 -0
  16. package/docs/runtime-truth.md +33 -8
  17. package/docs/security-edge-deepening-proof.md +1 -1
  18. package/examples/README.md +98 -69
  19. package/examples/complex_campus_master_edit_v4.inventory.json +12 -2
  20. package/examples/gallery.md +94 -6
  21. package/examples/home_iot_cli_edit_v1.inventory.json +11 -2
  22. package/examples/index.json +932 -4
  23. package/examples/local-sample-evidence.json +24 -0
  24. package/examples/proof-cards.json +117 -0
  25. package/examples/service_heavy_cli_edit_v1.inventory.json +11 -2
  26. package/package.json +60 -53
  27. package/pytest.ini +9 -0
  28. package/references/packettracer-sample-catalog.json +45287 -4525
  29. package/references/packettracer-sample-catalog.md +599 -259
  30. package/references/proof-readiness-candidates.json +352 -0
  31. package/scripts/build_examples_index.py +228 -35
  32. package/scripts/build_sample_catalog.py +24 -44
  33. package/scripts/corpus_runner.py +430 -0
  34. package/scripts/coverage_matrix.py +1842 -1812
  35. package/scripts/donor_cache.py +354 -0
  36. package/scripts/donor_diagnostics.py +3 -1
  37. package/scripts/generate_pkt.py +8762 -4228
  38. package/scripts/intent_parser.py +2242 -1657
  39. package/scripts/local_donors.py +340 -0
  40. package/scripts/packet_tracer_env.py +846 -391
  41. package/scripts/pkt_annotate.py +218 -0
  42. package/scripts/pkt_codec.py +420 -181
  43. package/scripts/pkt_editor.py +2405 -1703
  44. package/scripts/pkt_transformer.py +1072 -727
  45. package/scripts/pkt_verify.py +461 -0
  46. package/scripts/runtime_doctor.py +80 -29
  47. package/scripts/sample_catalog.py +1372 -1250
  48. package/scripts/twofish_diagnostics.py +48 -31
  49. package/scripts/usage_ledger.py +218 -0
  50. package/scripts/vendor/README.md +44 -37
  51. package/scripts/vendor/twofish_pure.py +321 -0
  52. package/scripts/workspace_repair.py +548 -508
  53. package/templates/pt900/donors/README.md +15 -0
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env python3
2
+ """Notes, frames and shapes on the logical workspace.
3
+
4
+ Packet Tracer's drawing tools are part of what makes a large lab readable: a
5
+ labelled frame around each building, a coloured box behind the server farm, a
6
+ note explaining what the topology demonstrates. None of that was reachable from
7
+ this skill.
8
+
9
+ Formats measured from Cisco's own bundled labs rather than guessed:
10
+
11
+ <RECTANGLE> <NOTE>
12
+ <TopLeftX>64</TopLeftX> <X>547</X>
13
+ <TopLeftY>174</TopLeftY> <Y>329</Y>
14
+ <BottomRightX>546</BottomRightX> <Z>40001</Z>
15
+ <BottomRightY>397</BottomRightY> <TEXT translate="true">...</TEXT>
16
+ <Color> <NOTECLUSTERID>1-1</NOTECLUSTERID>
17
+ <Red>0</Red> </NOTE>
18
+ <Green>0</Green>
19
+ <Blue>0</Blue>
20
+ </Color>
21
+ <Filled>0</Filled>
22
+ <RECTCLUSTERID>1-1</RECTCLUSTERID>
23
+ </RECTANGLE>
24
+
25
+ `Color` is the outline; `Filled` decides whether the interior is painted with
26
+ it. Ellipses share the shape exactly, with `ELLIPSECLUSTERID`. The containers --
27
+ `RECTANGLES`, `ELLIPSES`, `LINES`, `NOTES` -- sit directly under the document
28
+ root, not inside a device.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import uuid as _uuid
34
+ import xml.etree.ElementTree as ET
35
+
36
+ # Cluster ids group annotations with a logical-workspace cluster. `1-1` is the
37
+ # root cluster, which is what an unclustered lab uses throughout.
38
+ ROOT_CLUSTER = "1-1"
39
+
40
+ # A small named palette, because "qirmizi" is easier to write than an RGB triple.
41
+ COLORS: dict[str, tuple[int, int, int]] = {
42
+ "black": (0, 0, 0),
43
+ "white": (255, 255, 255),
44
+ "red": (220, 60, 60),
45
+ "green": (60, 180, 90),
46
+ "blue": (70, 120, 220),
47
+ "yellow": (250, 215, 80),
48
+ "orange": (245, 150, 60),
49
+ "purple": (160, 100, 210),
50
+ "grey": (150, 150, 150),
51
+ "gray": (150, 150, 150),
52
+ "lightblue": (170, 170, 255),
53
+ "lightgreen": (190, 235, 200),
54
+ "lightyellow": (255, 245, 190),
55
+ }
56
+ # Azerbaijani names for the same palette, so a prompt can say `qirmizi cerceve`.
57
+ COLORS.update({
58
+ "qara": COLORS["black"], "ag": COLORS["white"], "qirmizi": COLORS["red"],
59
+ "yasil": COLORS["green"], "mavi": COLORS["blue"], "sari": COLORS["yellow"],
60
+ "narinci": COLORS["orange"], "benovseyi": COLORS["purple"], "boz": COLORS["grey"],
61
+ })
62
+
63
+
64
+ def resolve_color(name: str | tuple[int, int, int] | None) -> tuple[int, int, int]:
65
+ """A colour name, an RGB triple, or black."""
66
+ if isinstance(name, (tuple, list)) and len(name) == 3:
67
+ return tuple(max(0, min(255, int(part))) for part in name) # type: ignore[return-value]
68
+ return COLORS.get(str(name or "").strip().lower(), COLORS["black"])
69
+
70
+
71
+ def _tag_with_uuid(parent: ET.Element, tag: str) -> ET.Element:
72
+ """Create an annotation element carrying the identifier Packet Tracer uses.
73
+
74
+ Packet Tracer stamps every shape it saves with a braced version-4 `uuid`.
75
+ Ours opened without one, but matching what the application writes keeps a
76
+ generated file indistinguishable from a hand-drawn one after a round trip.
77
+ """
78
+ node = ET.SubElement(parent, tag)
79
+ node.set("uuid", "{" + str(_uuid.uuid4()) + "}")
80
+ return node
81
+
82
+
83
+ def _container(root: ET.Element, tag: str) -> ET.Element:
84
+ """The document-level container for a kind of annotation, created if absent."""
85
+ node = root.find(tag)
86
+ if node is None:
87
+ node = ET.SubElement(root, tag)
88
+ return node
89
+
90
+
91
+ def _filled_node(parent: ET.Element, filled: bool, outline: str | tuple[int, int, int] | None) -> None:
92
+ """`Filled` also carries the outline, which is a separate colour.
93
+
94
+ Packet Tracer writes `<Filled OUTLINECOLOR="#000000" OUTLINED="false">1</Filled>`,
95
+ so a painted shape can have a border in a different colour -- something the
96
+ first version of this file had no way to express.
97
+ """
98
+ node = ET.SubElement(parent, "Filled")
99
+ node.text = "1" if filled else "0"
100
+ if outline is not None:
101
+ red, green, blue = resolve_color(outline)
102
+ node.set("OUTLINECOLOR", f"#{red:02x}{green:02x}{blue:02x}")
103
+ node.set("OUTLINED", "true")
104
+ else:
105
+ node.set("OUTLINECOLOR", "#000000")
106
+ node.set("OUTLINED", "false")
107
+
108
+
109
+ def _color_node(parent: ET.Element, color: tuple[int, int, int]) -> None:
110
+ node = ET.SubElement(parent, "Color")
111
+ for channel, value in zip(("Red", "Green", "Blue"), color):
112
+ ET.SubElement(node, channel).text = str(int(value))
113
+
114
+
115
+ def add_rectangle(
116
+ root: ET.Element,
117
+ top_left: tuple[float, float],
118
+ bottom_right: tuple[float, float],
119
+ *,
120
+ color: str | tuple[int, int, int] = "black",
121
+ filled: bool = False,
122
+ outline: str | tuple[int, int, int] | None = None,
123
+ ) -> ET.Element:
124
+ """A frame. `filled` paints the interior; `outline` borders it separately."""
125
+ rectangles = _container(root, "RECTANGLES")
126
+ rectangle = _tag_with_uuid(rectangles, "RECTANGLE")
127
+ ET.SubElement(rectangle, "TopLeftX").text = str(top_left[0])
128
+ ET.SubElement(rectangle, "TopLeftY").text = str(top_left[1])
129
+ ET.SubElement(rectangle, "BottomRightX").text = str(bottom_right[0])
130
+ ET.SubElement(rectangle, "BottomRightY").text = str(bottom_right[1])
131
+ _color_node(rectangle, resolve_color(color))
132
+ _filled_node(rectangle, filled, outline)
133
+ ET.SubElement(rectangle, "RECTCLUSTERID").text = ROOT_CLUSTER
134
+ return rectangle
135
+
136
+
137
+ def add_ellipse(
138
+ root: ET.Element,
139
+ top_left: tuple[float, float],
140
+ bottom_right: tuple[float, float],
141
+ *,
142
+ color: str | tuple[int, int, int] = "black",
143
+ filled: bool = False,
144
+ outline: str | tuple[int, int, int] | None = None,
145
+ ) -> ET.Element:
146
+ ellipses = _container(root, "ELLIPSES")
147
+ ellipse = _tag_with_uuid(ellipses, "ELLIPSE")
148
+ ET.SubElement(ellipse, "TopLeftX").text = str(top_left[0])
149
+ ET.SubElement(ellipse, "TopLeftY").text = str(top_left[1])
150
+ ET.SubElement(ellipse, "BottomRightX").text = str(bottom_right[0])
151
+ ET.SubElement(ellipse, "BottomRightY").text = str(bottom_right[1])
152
+ _color_node(ellipse, resolve_color(color))
153
+ _filled_node(ellipse, filled, outline)
154
+ ET.SubElement(ellipse, "ELLIPSECLUSTERID").text = ROOT_CLUSTER
155
+ return ellipse
156
+
157
+
158
+ def add_line(
159
+ root: ET.Element,
160
+ start: tuple[float, float],
161
+ end: tuple[float, float],
162
+ *,
163
+ color: str | tuple[int, int, int] = "black",
164
+ ) -> ET.Element:
165
+ """A straight line.
166
+
167
+ A line does not use the rectangle's corner fields. Writing it that way
168
+ produced lines that never appeared: Packet Tracer read the file, rewrote
169
+ every one into `StartX/StartY/EndX/EndY`, and dropped them from the view.
170
+ The real shape came from a lab where the drawing palette had been used.
171
+ """
172
+ lines = _container(root, "LINES")
173
+ line = _tag_with_uuid(lines, "LINE")
174
+ ET.SubElement(line, "StartX").text = str(start[0])
175
+ ET.SubElement(line, "StartY").text = str(start[1])
176
+ ET.SubElement(line, "EndX").text = str(end[0])
177
+ ET.SubElement(line, "EndY").text = str(end[1])
178
+ _color_node(line, resolve_color(color))
179
+ ET.SubElement(line, "LINECLUSTERID").text = ROOT_CLUSTER
180
+ return line
181
+
182
+
183
+ def add_note(root: ET.Element, position: tuple[float, float], text: str, *, z: int = 40001) -> ET.Element:
184
+ """A text note. `Z` sits above the devices so the note stays readable.
185
+
186
+ The container name is misleading: notes shown on the *logical* workspace are
187
+ stored under `PHYSICALWORKSPACE/NOTES`. Written at the document root instead,
188
+ Packet Tracer moved them and emptied the text, leaving notes parked at the
189
+ 50000,50000 sentinel with nothing in them.
190
+ """
191
+ workspace = root.find("PHYSICALWORKSPACE")
192
+ if workspace is None:
193
+ workspace = ET.SubElement(root, "PHYSICALWORKSPACE")
194
+ notes = workspace.find("NOTES")
195
+ if notes is None:
196
+ notes = ET.SubElement(workspace, "NOTES")
197
+ note = _tag_with_uuid(notes, "NOTE")
198
+ ET.SubElement(note, "X").text = str(position[0])
199
+ ET.SubElement(note, "Y").text = str(position[1])
200
+ ET.SubElement(note, "Z").text = str(z)
201
+ text_node = ET.SubElement(note, "TEXT")
202
+ text_node.set("translate", "true")
203
+ text_node.text = text
204
+ ET.SubElement(note, "NOTECLUSTERID").text = ROOT_CLUSTER
205
+ return note
206
+
207
+
208
+ def clear_annotations(root: ET.Element) -> None:
209
+ """Drop every annotation, so a regenerated lab does not inherit stale ones."""
210
+ for tag in ("RECTANGLES", "ELLIPSES", "LINES", "POLYGONS"):
211
+ node = root.find(tag)
212
+ if node is not None:
213
+ root.remove(node)
214
+ workspace = root.find("PHYSICALWORKSPACE")
215
+ if workspace is not None:
216
+ notes = workspace.find("NOTES")
217
+ if notes is not None:
218
+ workspace.remove(notes)