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.
- package/CHANGELOG.md +424 -73
- package/README.md +557 -442
- package/SKILL.md +337 -262
- package/bin/packet-tracer-skill.js +29 -2
- 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/l2-security-qos-proof.md +1 -1
- package/docs/packet-tracer-feature-gap-atlas.md +4 -4
- 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 +17 -8
- package/docs/release-notes-0.2.4.md +20 -0
- package/docs/runtime-truth.md +33 -8
- package/docs/security-edge-deepening-proof.md +1 -1
- 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 -53
- package/pytest.ini +9 -0
- 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 -1812
- package/scripts/donor_cache.py +354 -0
- package/scripts/donor_diagnostics.py +3 -1
- package/scripts/generate_pkt.py +8762 -4228
- package/scripts/intent_parser.py +2242 -1657
- 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 -1703
- package/scripts/pkt_transformer.py +1072 -727
- package/scripts/pkt_verify.py +461 -0
- package/scripts/runtime_doctor.py +80 -29
- package/scripts/sample_catalog.py +1372 -1250
- 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
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run a set of prompts end to end and record what actually happened.
|
|
3
|
+
|
|
4
|
+
Capability claims in this repo were hand-maintained for a long time, which is
|
|
5
|
+
how it ended up asserting things no one had measured. This runner produces the
|
|
6
|
+
evidence instead: prompt in, `.pkt` out, structural check, and optionally a real
|
|
7
|
+
Packet Tracer open. The result file is the input for any readiness claim.
|
|
8
|
+
|
|
9
|
+
python scripts/corpus_runner.py # structural tier only
|
|
10
|
+
python scripts/corpus_runner.py --open # also open each file in PT
|
|
11
|
+
python scripts/corpus_runner.py --case campus # one case
|
|
12
|
+
|
|
13
|
+
Results land in `output/corpus-results.json`, which is gitignored: this is
|
|
14
|
+
evidence about *this* machine's Packet Tracer, not a portable claim.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
from dataclasses import asdict, dataclass, field
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
|
28
|
+
sys.path.insert(0, str(SKILL_ROOT / "scripts"))
|
|
29
|
+
|
|
30
|
+
import pkt_verify # noqa: E402
|
|
31
|
+
from pkt_codec import decode_pkt_auto # noqa: E402
|
|
32
|
+
|
|
33
|
+
DEFAULT_RESULTS = SKILL_ROOT / "output" / "corpus-results.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class CorpusCase:
|
|
38
|
+
name: str
|
|
39
|
+
prompt: str
|
|
40
|
+
expects: str = "generate" # generate | refuse | donor_limited | capability_gap
|
|
41
|
+
note: str = ""
|
|
42
|
+
# Markers that must appear in the generated lab for the request to have been
|
|
43
|
+
# honoured. Without this, "verified" meant only that Packet Tracer opened the
|
|
44
|
+
# file -- which a donor-shaped lab does whether or not the prompt's
|
|
45
|
+
# capability was applied. Two cases passed that way: they asked for router
|
|
46
|
+
# DHCP and for server DNS/HTTP, produced neither, and were still counted as
|
|
47
|
+
# verified because the file opened.
|
|
48
|
+
requires_content: tuple[str, ...] = ()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Deliberately spans the shapes the donor cannot supply directly, because that
|
|
52
|
+
# is where `PACKET_TRACER_LINK_STRATEGY=create` is actually exercised.
|
|
53
|
+
CORPUS: tuple[CorpusCase, ...] = (
|
|
54
|
+
CorpusCase("minimal", "1 router 1 switch ve 3 komputer qur"),
|
|
55
|
+
CorpusCase("two_switch_chain", "2 switch 1 router ve 4 komputer qur"),
|
|
56
|
+
CorpusCase(
|
|
57
|
+
"voice_devices",
|
|
58
|
+
"1 router 1 switch 1 analog telefon 1 home voip qur",
|
|
59
|
+
note=(
|
|
60
|
+
"analog and home VoIP phones. Both live only in donors whose sole "
|
|
61
|
+
"switch is Layer-3, which reported no switch groups at all, so every "
|
|
62
|
+
"device in those labs was unreachable however the prompt was worded"
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
CorpusCase(
|
|
66
|
+
"hosts_across_switches",
|
|
67
|
+
"3 switch 1 router ve 4 komputer qur",
|
|
68
|
+
requires_content=("switchport mode trunk",),
|
|
69
|
+
note=(
|
|
70
|
+
"hosts on more than one access switch. No case did this, which is why "
|
|
71
|
+
"cloned switches sharing one bridge address went unnoticed: they "
|
|
72
|
+
"announced themselves as a single bridge and only one could reach the "
|
|
73
|
+
"core, so anything behind the others was cut off"
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
CorpusCase(
|
|
77
|
+
"campus_star_vlan",
|
|
78
|
+
"3 dene switch ve 6 komputer ve 1 router vlanlarda 10,20,30",
|
|
79
|
+
requires_content=("vlan 10", "vlan 20", "vlan 30"),
|
|
80
|
+
note="star target on a chain donor; needs a created link",
|
|
81
|
+
),
|
|
82
|
+
CorpusCase(
|
|
83
|
+
"four_switch",
|
|
84
|
+
"4 switch 1 router 8 komputer qur",
|
|
85
|
+
note="four switches on a three-switch donor; needs group duplication",
|
|
86
|
+
),
|
|
87
|
+
CorpusCase("server_lan", "1 router 1 switch 2 komputer 1 server qur"),
|
|
88
|
+
CorpusCase(
|
|
89
|
+
"hosts_only",
|
|
90
|
+
"1 switch ve 5 komputer qur",
|
|
91
|
+
note="more hosts on one switch than the donor group has; needs host duplication",
|
|
92
|
+
),
|
|
93
|
+
CorpusCase(
|
|
94
|
+
"vlan_uneven",
|
|
95
|
+
"2 switch 1 router 7 komputer vlanlarda 10,20",
|
|
96
|
+
requires_content=("vlan 10", "vlan 20"),
|
|
97
|
+
note="uneven split; the busier switch needs more hosts than the donor gives",
|
|
98
|
+
),
|
|
99
|
+
CorpusCase(
|
|
100
|
+
"no_devices",
|
|
101
|
+
"sebeke haqqinda melumat ver",
|
|
102
|
+
expects="refuse",
|
|
103
|
+
note="not a topology request; must not invent one",
|
|
104
|
+
),
|
|
105
|
+
# Beyond plain topology: the capability surface the skill advertises but the
|
|
106
|
+
# corpus has never exercised end to end.
|
|
107
|
+
CorpusCase(
|
|
108
|
+
"vlan_explicit_split",
|
|
109
|
+
"2 switch 1 router 6 komputer vlan 10 da 4 pc vlan 20 de 2 pc",
|
|
110
|
+
requires_content=("vlan 10", "vlan 20"),
|
|
111
|
+
note="explicit host-to-VLAN counts rather than an even default",
|
|
112
|
+
),
|
|
113
|
+
CorpusCase(
|
|
114
|
+
"router_dhcp",
|
|
115
|
+
"1 router 1 switch 3 komputer qur dhcp routerden verilsin",
|
|
116
|
+
requires_content=("ip dhcp pool",),
|
|
117
|
+
note="router DHCP pool on a flat network, the case the VLAN path misses",
|
|
118
|
+
),
|
|
119
|
+
CorpusCase(
|
|
120
|
+
"server_services",
|
|
121
|
+
"1 router 1 switch 2 komputer 1 server qur serverde dns ve http olsun",
|
|
122
|
+
# `www.local` is the record this skill writes. A bare `<DNS_SERVER>`
|
|
123
|
+
# marker would pass on donor content alone and prove nothing.
|
|
124
|
+
requires_content=("www.local",),
|
|
125
|
+
note="server service enablement plus a DNS record",
|
|
126
|
+
),
|
|
127
|
+
CorpusCase(
|
|
128
|
+
"management_telnet",
|
|
129
|
+
"2 switch 1 router 4 komputer qur management vlan 99 ve telnet olsun",
|
|
130
|
+
# `username admin secret cisco` is what enable_telnet writes; the donor's
|
|
131
|
+
# own vty lines would satisfy a looser marker without proving anything.
|
|
132
|
+
requires_content=("interface vlan99", "username admin secret cisco"),
|
|
133
|
+
note="management SVI plus telnet credentials on every switch",
|
|
134
|
+
),
|
|
135
|
+
CorpusCase(
|
|
136
|
+
"two_routers",
|
|
137
|
+
"2 router 2 switch 4 pc qur",
|
|
138
|
+
# The device name is the proof: R2 exists only if the second router was
|
|
139
|
+
# actually created. This silently produced one router for a long time --
|
|
140
|
+
# the file opened, so nothing reported the loss. The closing tag is
|
|
141
|
+
# matched without the opening one because Packet Tracer writes
|
|
142
|
+
# `<NAME translate="true">`.
|
|
143
|
+
requires_content=(">R2</NAME>",),
|
|
144
|
+
note="more routers than the donor has; needs router duplication",
|
|
145
|
+
),
|
|
146
|
+
CorpusCase(
|
|
147
|
+
"bare_device_names",
|
|
148
|
+
"router switch pc qur",
|
|
149
|
+
note="devices named with no counts; one of each is the only sensible reading",
|
|
150
|
+
),
|
|
151
|
+
CorpusCase(
|
|
152
|
+
"hosts_without_a_switch",
|
|
153
|
+
"bir sebeke lazimdir 10 kompyuter ucun",
|
|
154
|
+
note="hosts named with nothing to plug into; a switch has to be inferred",
|
|
155
|
+
),
|
|
156
|
+
CorpusCase(
|
|
157
|
+
"named_model",
|
|
158
|
+
"2911 router ve 2960 switch ile 3 pc qur",
|
|
159
|
+
# The model number sits exactly where a count goes; this asked for two
|
|
160
|
+
# thousand nine hundred and eleven routers and planned for minutes.
|
|
161
|
+
note="device models named in the prompt must not be read as counts",
|
|
162
|
+
),
|
|
163
|
+
CorpusCase(
|
|
164
|
+
"ospf_routing",
|
|
165
|
+
"2 router 3 switch 6 komputer qur vlanlarda 10,20 ospf olsun",
|
|
166
|
+
requires_content=("router ospf",),
|
|
167
|
+
note="routing was refused by a hand-maintained acceptance table until measured",
|
|
168
|
+
),
|
|
169
|
+
CorpusCase(
|
|
170
|
+
"eigrp_routing",
|
|
171
|
+
"2 router 2 switch 4 komputer qur eigrp olsun",
|
|
172
|
+
requires_content=("router eigrp",),
|
|
173
|
+
note="the same emission path as OSPF, different protocol",
|
|
174
|
+
),
|
|
175
|
+
CorpusCase(
|
|
176
|
+
"nat_internet",
|
|
177
|
+
"1 router 1 switch 4 komputer qur nat olsun",
|
|
178
|
+
requires_content=("ip nat",),
|
|
179
|
+
note="NAT overload plus the ACL that feeds it",
|
|
180
|
+
),
|
|
181
|
+
CorpusCase(
|
|
182
|
+
"acl_security",
|
|
183
|
+
"2 router 2 switch 6 komputer qur acl olsun",
|
|
184
|
+
requires_content=("access-list",),
|
|
185
|
+
note="named standard ACL with a permit rule",
|
|
186
|
+
),
|
|
187
|
+
CorpusCase(
|
|
188
|
+
"stp_hardening",
|
|
189
|
+
"2 switch 1 router 4 komputer qur stp olsun",
|
|
190
|
+
requires_content=("spanning-tree",),
|
|
191
|
+
note="rapid-pvst with the core as root",
|
|
192
|
+
),
|
|
193
|
+
CorpusCase(
|
|
194
|
+
"hsrp_redundancy",
|
|
195
|
+
"2 router 2 switch 6 komputer qur hsrp olsun",
|
|
196
|
+
requires_content=("standby",),
|
|
197
|
+
note="first-hop redundancy; needs a virtual address or it configures nothing",
|
|
198
|
+
),
|
|
199
|
+
CorpusCase(
|
|
200
|
+
"ipv6_dual_stack",
|
|
201
|
+
"2 router 2 switch 6 komputer qur ipv6 ve ospfv3 olsun",
|
|
202
|
+
requires_content=("ipv6 unicast-routing", "2001:db8"),
|
|
203
|
+
note="plain `ipv6` was not even a capability until measured",
|
|
204
|
+
),
|
|
205
|
+
CorpusCase(
|
|
206
|
+
"voip_telephony",
|
|
207
|
+
"1 router 2 switch 4 komputer qur voip olsun",
|
|
208
|
+
requires_content=("telephony-service",),
|
|
209
|
+
note="Call Manager Express with a directory number per phone",
|
|
210
|
+
),
|
|
211
|
+
CorpusCase(
|
|
212
|
+
"server_farm_services",
|
|
213
|
+
"1 router 1 switch 2 komputer 1 server qur serverde ftp ve tftp olsun",
|
|
214
|
+
requires_content=("ftp",),
|
|
215
|
+
note="the services beyond dns/http that the enable map already knew",
|
|
216
|
+
),
|
|
217
|
+
CorpusCase(
|
|
218
|
+
"management_services",
|
|
219
|
+
"1 router 1 switch 1 server qur ntp ve syslog olsun",
|
|
220
|
+
requires_content=("ntp", "syslog"),
|
|
221
|
+
note="refused for a wiring preference nobody expressed; blocked four capabilities",
|
|
222
|
+
),
|
|
223
|
+
CorpusCase(
|
|
224
|
+
"gre_tunnel",
|
|
225
|
+
"2 router 2 switch 4 komputer qur gre tunnel olsun",
|
|
226
|
+
requires_content=("interface tunnel",),
|
|
227
|
+
note="refused by a coverage table while the lab it would build opened fine",
|
|
228
|
+
),
|
|
229
|
+
CorpusCase(
|
|
230
|
+
"multiarea_ospf",
|
|
231
|
+
"3 router 2 switch 6 komputer qur multi area ospf olsun vlanlarda 10,20,30",
|
|
232
|
+
requires_content=("area 1",),
|
|
233
|
+
note="every OSPF lab was single-area whatever the prompt asked",
|
|
234
|
+
),
|
|
235
|
+
CorpusCase(
|
|
236
|
+
"dhcp_snooping",
|
|
237
|
+
"2 switch 1 router 4 komputer qur dhcp snooping olsun",
|
|
238
|
+
requires_content=("ip dhcp snooping",),
|
|
239
|
+
note="recognised by the parser, emitted by nobody",
|
|
240
|
+
),
|
|
241
|
+
CorpusCase(
|
|
242
|
+
"wireless_ssid",
|
|
243
|
+
"1 wireless router 2 laptop qur ssid EvSebeke wpa2 sifre Gizli123",
|
|
244
|
+
# The network name and secret are the proof. Without the wireless ops the
|
|
245
|
+
# lab keeps whatever network the donor was configured with.
|
|
246
|
+
requires_content=("EvSebeke", "Gizli123"),
|
|
247
|
+
note="names the wireless network and puts the clients on it",
|
|
248
|
+
),
|
|
249
|
+
CorpusCase(
|
|
250
|
+
"wireless_home",
|
|
251
|
+
"1 wireless router 2 laptop qur",
|
|
252
|
+
# Was reported as a donor limitation for weeks. It was a donor
|
|
253
|
+
# *selection* limitation: the base pool held one lab, and this machine
|
|
254
|
+
# had wireless-capable labs all along.
|
|
255
|
+
requires_content=(">WRT1</NAME>", ">Laptop2</NAME>"),
|
|
256
|
+
note="needs a donor the primary pool does not contain; exercises local-lab widening",
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass
|
|
262
|
+
class CaseResult:
|
|
263
|
+
name: str
|
|
264
|
+
prompt: str
|
|
265
|
+
expects: str
|
|
266
|
+
note: str = ""
|
|
267
|
+
generated: bool = False
|
|
268
|
+
structural_passed: bool | None = None
|
|
269
|
+
device_count: int = 0
|
|
270
|
+
link_count: int = 0
|
|
271
|
+
version: str = ""
|
|
272
|
+
container: str = ""
|
|
273
|
+
open_status: str = ""
|
|
274
|
+
seconds: float = 0.0
|
|
275
|
+
outcome: str = ""
|
|
276
|
+
detail: str = ""
|
|
277
|
+
failures: list[str] = field(default_factory=list)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _missing_content(pkt_path: Path, required: tuple[str, ...]) -> list[str]:
|
|
281
|
+
"""Markers the prompt asked for that the generated lab does not contain."""
|
|
282
|
+
try:
|
|
283
|
+
decoded = decode_pkt_auto(pkt_path.read_bytes())
|
|
284
|
+
except Exception as error: # noqa: BLE001 - reported, not raised
|
|
285
|
+
return [f"could not decode to check content ({error})"]
|
|
286
|
+
payload = decoded[0] if isinstance(decoded, tuple) else decoded
|
|
287
|
+
text = payload.decode("utf-8", "replace").lower()
|
|
288
|
+
return [marker for marker in required if marker.lower() not in text]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def run_case(case: CorpusCase, output_dir: Path, do_open: bool, timeout: int) -> CaseResult:
|
|
292
|
+
result = CaseResult(name=case.name, prompt=case.prompt, expects=case.expects, note=case.note)
|
|
293
|
+
output_path = output_dir / f"corpus_{case.name}.pkt"
|
|
294
|
+
output_path.unlink(missing_ok=True)
|
|
295
|
+
|
|
296
|
+
started = time.monotonic()
|
|
297
|
+
completed = subprocess.run(
|
|
298
|
+
[sys.executable, str(SKILL_ROOT / "scripts" / "generate_pkt.py"),
|
|
299
|
+
"--prompt", case.prompt, "--output", str(output_path)],
|
|
300
|
+
capture_output=True,
|
|
301
|
+
text=True,
|
|
302
|
+
cwd=str(SKILL_ROOT),
|
|
303
|
+
)
|
|
304
|
+
result.seconds = round(time.monotonic() - started, 1)
|
|
305
|
+
result.generated = output_path.exists()
|
|
306
|
+
|
|
307
|
+
if not result.generated:
|
|
308
|
+
result.detail = (completed.stdout or completed.stderr or "").strip()[-400:]
|
|
309
|
+
if case.expects == "refuse":
|
|
310
|
+
result.outcome = "refused_as_expected"
|
|
311
|
+
elif case.expects == "capability_gap":
|
|
312
|
+
# Recognised in the prompt, not implemented for generation. Distinct
|
|
313
|
+
# from a donor limit: a richer donor would not help.
|
|
314
|
+
result.outcome = "refused_capability_gap"
|
|
315
|
+
elif case.expects == "donor_limited":
|
|
316
|
+
# Not a defect: the request is sound and the local donor cannot serve
|
|
317
|
+
# it. Kept visible rather than folded into "expected" so the gap stays
|
|
318
|
+
# countable, but it does not fail the run.
|
|
319
|
+
result.outcome = "refused_donor_limited"
|
|
320
|
+
else:
|
|
321
|
+
result.outcome = "unexpected_refusal"
|
|
322
|
+
return result
|
|
323
|
+
|
|
324
|
+
if case.expects == "refuse":
|
|
325
|
+
result.outcome = "unexpected_generation"
|
|
326
|
+
|
|
327
|
+
report = pkt_verify.structural_check(output_path)
|
|
328
|
+
result.structural_passed = report.passed
|
|
329
|
+
result.device_count = report.device_count
|
|
330
|
+
result.link_count = report.link_count
|
|
331
|
+
result.version = report.version
|
|
332
|
+
result.container = report.container
|
|
333
|
+
result.failures = report.failures
|
|
334
|
+
|
|
335
|
+
if not report.passed:
|
|
336
|
+
result.outcome = result.outcome or "structural_failed"
|
|
337
|
+
return result
|
|
338
|
+
|
|
339
|
+
if case.requires_content:
|
|
340
|
+
missing = _missing_content(output_path, case.requires_content)
|
|
341
|
+
if missing:
|
|
342
|
+
# The file opens, but it does not do what was asked. Counting this as
|
|
343
|
+
# verified is how two capability gaps hid in plain sight.
|
|
344
|
+
result.failures.append(
|
|
345
|
+
"generated lab is missing what the prompt asked for: " + ", ".join(missing)
|
|
346
|
+
)
|
|
347
|
+
result.outcome = result.outcome or (
|
|
348
|
+
"refused_capability_gap" if case.expects == "capability_gap" else "content_missing"
|
|
349
|
+
)
|
|
350
|
+
return result
|
|
351
|
+
if case.expects == "capability_gap":
|
|
352
|
+
result.outcome = "unexpected_capability"
|
|
353
|
+
|
|
354
|
+
if do_open:
|
|
355
|
+
open_report = pkt_verify.open_check(output_path, timeout_seconds=timeout)
|
|
356
|
+
result.open_status = open_report.status
|
|
357
|
+
result.outcome = result.outcome or ("verified" if open_report.opened else "generated_unverified")
|
|
358
|
+
else:
|
|
359
|
+
result.outcome = result.outcome or "generated_unverified"
|
|
360
|
+
return result
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def main() -> int:
|
|
364
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
365
|
+
parser.add_argument("--open", action="store_true", help="launch Packet Tracer for each generated file")
|
|
366
|
+
parser.add_argument("--case", action="append", help="run only these cases (repeatable)")
|
|
367
|
+
parser.add_argument("--timeout", type=int, default=180)
|
|
368
|
+
parser.add_argument("--results", type=Path, default=DEFAULT_RESULTS)
|
|
369
|
+
args = parser.parse_args()
|
|
370
|
+
|
|
371
|
+
selected = [case for case in CORPUS if not args.case or case.name in args.case]
|
|
372
|
+
if not selected:
|
|
373
|
+
print(f"no cases matched {args.case}", file=sys.stderr)
|
|
374
|
+
return 2
|
|
375
|
+
|
|
376
|
+
output_dir = SKILL_ROOT / "output"
|
|
377
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
378
|
+
|
|
379
|
+
results = []
|
|
380
|
+
for case in selected:
|
|
381
|
+
result = run_case(case, output_dir, args.open, args.timeout)
|
|
382
|
+
results.append(result)
|
|
383
|
+
flag = {
|
|
384
|
+
"verified": "OK ",
|
|
385
|
+
"generated_unverified": "GEN ",
|
|
386
|
+
"refused_as_expected": "REF ",
|
|
387
|
+
"refused_donor_limited": "GAP ",
|
|
388
|
+
"refused_capability_gap": "CAP ",
|
|
389
|
+
}.get(result.outcome, "FAIL")
|
|
390
|
+
print(
|
|
391
|
+
f"{flag} {result.name:20} {result.outcome:22} "
|
|
392
|
+
f"devices={result.device_count:<3} links={result.link_count:<3} "
|
|
393
|
+
f"open={result.open_status or '-':16} {result.seconds}s"
|
|
394
|
+
)
|
|
395
|
+
if result.failures:
|
|
396
|
+
for failure in result.failures[:3]:
|
|
397
|
+
print(f" {failure}")
|
|
398
|
+
|
|
399
|
+
payload = {
|
|
400
|
+
"cases": [asdict(item) for item in results],
|
|
401
|
+
"verified": sum(1 for item in results if item.outcome == "verified"),
|
|
402
|
+
"donor_limited": sum(1 for item in results if item.outcome == "refused_donor_limited"),
|
|
403
|
+
"capability_gap": sum(1 for item in results if item.outcome == "refused_capability_gap"),
|
|
404
|
+
"generated": sum(1 for item in results if item.generated),
|
|
405
|
+
"unexpected": sum(
|
|
406
|
+
1
|
|
407
|
+
for item in results
|
|
408
|
+
if item.outcome
|
|
409
|
+
in {
|
|
410
|
+
"unexpected_refusal",
|
|
411
|
+
"unexpected_generation",
|
|
412
|
+
"structural_failed",
|
|
413
|
+
"content_missing",
|
|
414
|
+
"unexpected_capability",
|
|
415
|
+
}
|
|
416
|
+
),
|
|
417
|
+
"total": len(results),
|
|
418
|
+
}
|
|
419
|
+
args.results.parent.mkdir(parents=True, exist_ok=True)
|
|
420
|
+
args.results.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
421
|
+
print(
|
|
422
|
+
f"\n{payload['generated']}/{payload['total']} generated, "
|
|
423
|
+
f"{payload['verified']} opened in Packet Tracer, "
|
|
424
|
+
f"{payload['unexpected']} unexpected. Results: {args.results}"
|
|
425
|
+
)
|
|
426
|
+
return 1 if payload["unexpected"] else 0
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
if __name__ == "__main__":
|
|
430
|
+
raise SystemExit(main())
|