vmware-debug 1.9.0__tar.gz → 1.11.0__tar.gz

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 (82) hide show
  1. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/.claude-plugin/plugin.json +1 -1
  2. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/.mcp.json +1 -1
  3. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/PKG-INFO +1 -1
  4. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/RELEASE_NOTES.md +84 -0
  5. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/pyproject.toml +1 -1
  6. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/server.json +11 -2
  7. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/SKILL.md +52 -3
  8. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/agent-guardrails.md +1 -1
  9. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/capabilities.md +8 -1
  10. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/event-envelope.md +1 -1
  11. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/routing.md +1 -0
  12. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/_scores.json +54 -53
  13. vmware_debug-1.11.0/tests/eval/regression/test_case_id_non_ascii.py +86 -0
  14. vmware_debug-1.11.0/tests/eval/regression/test_evidence_payload_contract.py +124 -0
  15. vmware_debug-1.11.0/tests/eval/regression/test_host_lifecycle_classification.py +244 -0
  16. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/regression/test_investigation_metrics.py +103 -4
  17. vmware_debug-1.11.0/tests/eval/regression/test_skill_name_aliases.py +124 -0
  18. vmware_debug-1.11.0/tests/eval/regression/test_spike_resolution.py +171 -0
  19. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_grading.py +29 -4
  20. vmware_debug-1.11.0/tests/test_knowledge.py +237 -0
  21. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_timeline.py +5 -1
  22. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/uv.lock +1 -1
  23. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/__init__.py +1 -1
  24. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/mcp/tools.py +2 -0
  25. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/mcp_server/server.py +91 -21
  26. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/api.py +49 -1
  27. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/evidence.py +8 -0
  28. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/grading.py +90 -3
  29. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/ids.py +25 -7
  30. vmware_debug-1.11.0/vmware_debug/ops/cases/knowledge.py +375 -0
  31. vmware_debug-1.11.0/vmware_debug/ops/cases/payloads.py +110 -0
  32. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/plan.py +48 -13
  33. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/readiness.py +43 -15
  34. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/store.py +8 -4
  35. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/timeline.py +26 -29
  36. vmware_debug-1.11.0/vmware_debug/ops/skill_names.py +92 -0
  37. vmware_debug-1.11.0/vmware_debug/ops/timeline.py +568 -0
  38. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/rules/evidence_sources.yaml +9 -0
  39. vmware_debug-1.9.0/vmware_debug/ops/timeline.py +0 -326
  40. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/.gitignore +0 -0
  41. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/README-CN.md +0 -0
  42. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/README.md +0 -0
  43. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/SECURITY.md +0 -0
  44. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/docs/plans/2026-08-28-investigation-layer-design.md +0 -0
  45. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/cli-reference.md +0 -0
  46. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/setup-guide.md +0 -0
  47. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/__init__.py +0 -0
  48. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/__init__.py +0 -0
  49. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/_family.py +0 -0
  50. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/_scoring.py +0 -0
  51. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/_skill.py +0 -0
  52. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/conftest.py +0 -0
  53. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/test_entity_reachability.py +0 -0
  54. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/test_error_actionability.py +0 -0
  55. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/test_tool_description_quality.py +0 -0
  56. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
  57. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/regression/__init__.py +0 -0
  58. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/regression/test_capability_grader.py +0 -0
  59. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/regression/test_debug_regressions.py +0 -0
  60. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/regression/test_result_envelope.py +0 -0
  61. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/regression/test_tool_annotations.py +0 -0
  62. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_conclusion.py +0 -0
  63. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_evidence.py +0 -0
  64. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_hypotheses.py +0 -0
  65. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_ids.py +0 -0
  66. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_plan.py +0 -0
  67. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_readiness.py +0 -0
  68. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_store.py +0 -0
  69. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_timeline_close.py +0 -0
  70. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_tools.py +0 -0
  71. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_safe_error_passthrough.py +0 -0
  72. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/cli.py +0 -0
  73. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/envelope.py +0 -0
  74. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/mcp/__init__.py +0 -0
  75. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/mcp_server/__init__.py +0 -0
  76. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/__init__.py +0 -0
  77. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/__init__.py +0 -0
  78. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/conclusion.py +0 -0
  79. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/hypotheses.py +0 -0
  80. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/model.py +0 -0
  81. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/sources.py +0 -0
  82. {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/rules/grading_rules.yaml +0 -0
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "vmware-debug",
4
4
  "displayName": "VMware Debug",
5
- "version": "1.9.0",
5
+ "version": "1.11.0",
6
6
  "description": "Read-only VMware incident correlation: timeline, spikes, root-cause routing. 2 MCP tools.",
7
7
  "author": {
8
8
  "name": "Wei Zhou",
@@ -5,7 +5,7 @@
5
5
  "command": "uvx",
6
6
  "args": [
7
7
  "--from",
8
- "vmware-debug==1.9.0",
8
+ "vmware-debug==1.11.0",
9
9
  "vmware-debug-mcp"
10
10
  ]
11
11
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vmware-debug
3
- Version: 1.9.0
3
+ Version: 1.11.0
4
4
  Summary: VMware diagnostic brain — read-only incident triage, log/event correlation, and root-cause routing across the VMware skill family
5
5
  Project-URL: Homepage, https://github.com/vmware-skills/VMware-Debug
6
6
  Project-URL: Repository, https://github.com/vmware-skills/VMware-Debug
@@ -1,3 +1,87 @@
1
+ ## v1.11.0 — the correlation engine, on a real incident
2
+
3
+ A tester fed the investigation layer a genuine failure: on 08-03 at 05:49, four
4
+ ESXi hosts went `EnteringMaintenanceMode` → `EnteredMaintenanceMode` →
5
+ `HostShutdown` → `HostConnectionLost` → `HostSyncFailed` ×24 within 32 seconds,
6
+ and never came back. 3,818 real vCenter events over 40 days, pulled per this
7
+ skill's own envelope documentation. The case-management layer held up. The
8
+ correlation engine — the thing this skill is named for — did not.
9
+
10
+ **80% of events were `uncategorized`, and the headline conclusion was "cannot
11
+ classify, widen your search window".** The classification table's
12
+ `power_lifecycle` keywords were all VM-centric (`power on`/`power off`, `vmx`,
13
+ `ovf`, `clone`, `snapshot`); nothing covered host maintenance mode or host
14
+ shutdown, and `"Shut down of esxi05"` does not match `"power off"`. There is now
15
+ a `host_lifecycle` category — a separate category rather than more keywords,
16
+ because it is a separate investigation, routed to monitor + logs + harden (a
17
+ host that left service on cue was usually told to). `classification_coverage()`
18
+ reports what went unread in every result, and above ~25% it reaches
19
+ `next_checks`. The "widen the window" remedy is gone: the window was already 40
20
+ days, and widening makes the incident *less* findable.
21
+
22
+ **The only spike detected was the day the hosts came back up.**
23
+ `_auto_bin_seconds` was `span/30`, so a 29-day window gave 23.2-hour buckets;
24
+ the incident hour was the second-busiest in the dataset and absent from the
25
+ spike list, while `--bin-seconds 3600` found it at z=8.25. Binning now chooses
26
+ by event *density*, from a floor of 4 events per bin that is derived rather than
27
+ tuned: spikes flag at mean+2σ, and a count only exceeds twice its mean once the
28
+ mean reaches 4. The chosen width, how it was chosen, and the bin count are
29
+ reported.
30
+
31
+ **The same incident described five ways routed five different ways** — decided
32
+ by an incidental noun, with the most accurate description routing nowhere.
33
+ Categories are now ranked by matched-keyword length, so a long phrase beats a
34
+ stray word, and the losing categories are named.
35
+
36
+ Also fixed: `case_timeline`'s payload contract was undocumented and its MCP
37
+ parameter type refused the array shape the docstring implied; pure-Chinese
38
+ summaries collapsed to one `case_id` per second, with a collision message that
39
+ was false; and skill names were compared as raw strings, so `monitor` and
40
+ `vmware-monitor` counted as two independent evidence sources — buying *Probable*
41
+ out of orthography, in the layer built to prevent exactly that.
42
+
43
+ ## v1.10.0 — the knowledge layer is implemented, not just described
44
+
45
+ v1.9.0 shipped the investigation layer with `applies_to` documented as the thing
46
+ that makes a knowledge entry decisive. Nothing parsed a knowledge file and
47
+ nothing checked applicability. Any file at all under the knowledge root raised
48
+ the reported ceiling, and anything labelled `knowledge-kb` counted as decisive.
49
+ The route to a wrong Confirmed that the design was written to close was open the
50
+ whole time, and the metric said zero only because no fixture walked it.
51
+
52
+ `case_knowledge` (the 14th tool) answers the first question anyone mounting a
53
+ library asks — which formats are taken — without them reading a design document:
54
+
55
+ .md .markdown YAML front-matter between --- fences, body below (preferred)
56
+ .yaml .yml the whole file is one entry
57
+ .json one entry per file
58
+ .jsonl one JSON object per line (what ticketing systems export)
59
+ .csv .tsv one entry per row; dotted columns for nested constraints
60
+ .txt .log a sibling <name>.yaml carries the metadata
61
+
62
+ PDF, DOCX, PPTX and HTML are named as needing conversion rather than ignored,
63
+ and a file that cannot be parsed is reported rather than skipped.
64
+
65
+ **An entry is decisive only if its own `applies_to` was checked against the
66
+ case's recorded versions and passed.** Three things follow, each found by
67
+ reviewing the version before it:
68
+
69
+ * Knowledge evidence must say WHICH entry it is. "Some applicable entry is
70
+ mounted somewhere" is a different claim from "this one applies", and the first
71
+ version accepted it — an agent citing the entry written for 9.x could confirm
72
+ a case about 8.0.3 as long as an unrelated entry happened to match.
73
+ * A constraint the case scope cannot answer is not a match. Silence is not a
74
+ pass; reading it as one is how an entry for different hardware ends a case.
75
+ * **A constraint the checker cannot evaluate is not a pass either.** The first
76
+ version understood product, build, driver and firmware and treated every other
77
+ key as satisfied, so `build` without a `product`, a `hardware_model` list, and
78
+ an outright typo all came back decisive. Unknown and satisfied must never be
79
+ the same answer.
80
+
81
+ Given two entries that read identically — one for `>=8.0, <9.0`, one for
82
+ `>=9.0` — a case scoped to vSphere 8.0.3 now reaches Confirmed citing the first
83
+ and stays at Probable citing the second, naming the range that excluded it.
84
+
1
85
  ## v1.9.0 — the investigation layer: 2 tools become 13
2
86
 
3
87
  Correlating events answers "what happened together". A case answers "what do we
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vmware-debug"
7
- version = "1.9.0"
7
+ version = "1.11.0"
8
8
  description = "VMware diagnostic brain — read-only incident triage, log/event correlation, and root-cause routing across the VMware skill family"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -7,12 +7,21 @@
7
7
  "url": "https://github.com/vmware-skills/VMware-Debug",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.9.0",
10
+ "version": "1.11.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "vmware-debug",
15
- "version": "1.9.0",
15
+ "version": "1.11.0",
16
+ "runtimeHint": "uvx",
17
+ "packageArguments": [
18
+ {
19
+ "type": "positional",
20
+ "valueHint": "subcommand",
21
+ "value": "mcp",
22
+ "description": "Starts the stdio MCP server. Without it the console script runs the human CLI and exits."
23
+ }
24
+ ],
16
25
  "transport": {
17
26
  "type": "stdio"
18
27
  }
@@ -100,7 +100,7 @@ a recommended plan.
100
100
  - **MCP** (in an agent): the agent calls the other skills' read tools, then `incident_timeline` to correlate. This is the primary mode — that's where the cross-skill "联动" happens.
101
101
  - **CLI** (humans): `vmware-debug triage --events events.json` correlates a JSON array you collected yourself.
102
102
 
103
- ## MCP Tools (13 — 6 read, 7 write)
103
+ ## MCP Tools (14 — 7 read, 7 write)
104
104
 
105
105
  **Correlation** — stateless, for a single look:
106
106
 
@@ -115,6 +115,7 @@ a recommended plan.
115
115
  |---|---|
116
116
  | `case_open` | [WRITE] Define the event; returns a case id and the grade this environment can reach |
117
117
  | `case_readiness` | [READ] What grade this environment can reach, per symptom category, **before** you start |
118
+ | `case_knowledge` | [READ] Which knowledge formats are accepted, what is mounted, and which entries apply to a case |
118
119
  | `case_plan` | [READ] What to fetch next — skill, tool and purpose per step; recomputed from the case's current state |
119
120
  | `case_list` | [READ] Cases, newest first |
120
121
  | `case_get` | [READ] One case: scope, ledger sizes, grade history |
@@ -179,6 +180,52 @@ so the gap is visible now rather than when the conclusion refuses to firm up.
179
180
  "readiness 78%" cannot. Two classes served by the *same* skill count as one
180
181
  source, so it agrees with what `case_grade` will actually award.
181
182
 
183
+ ### Mounting a knowledge library
184
+
185
+ `case_knowledge` answers "what can I add" without anyone reading this file.
186
+ Entries go under `$OPS_HOME/knowledge/{kb,runbook,sr,cases}/`:
187
+
188
+ | Format | How metadata travels |
189
+ |---|---|
190
+ | `.md` `.markdown` | YAML front-matter between `---` fences, body below — **preferred** |
191
+ | `.yaml` `.yml` | the whole file is one entry |
192
+ | `.json` | one entry per file |
193
+ | `.jsonl` | one JSON object per line — what ticketing systems export |
194
+ | `.csv` `.tsv` | one entry per row; use dotted columns (`driver.version`) for nested constraints |
195
+ | `.txt` `.log` | a sibling `<name>.yaml` carries the metadata |
196
+
197
+ PDF, DOCX, PPTX and HTML must be converted to Markdown first — `case_knowledge`
198
+ names them rather than ignoring them.
199
+
200
+ **Every entry needs an `applies_to` block to be decisive:**
201
+
202
+ ```yaml
203
+ ---
204
+ id: KB-2026-0417
205
+ applies_to:
206
+ product: vsphere
207
+ build: ">=8.0.3, <9.0"
208
+ driver: {name: nvme_pcie, version: ">=1.2.4"}
209
+ firmware: {vendor: dell, version: ">=52.26"}
210
+ ---
211
+ ```
212
+
213
+ `product`, `build`, `driver` and `firmware` are the constraints the checker
214
+ evaluates. **Any other key leaves the entry non-decisive** — including a typo —
215
+ because an unverified constraint is not a satisfied one, and `case_knowledge`
216
+ names the key it could not check.
217
+
218
+ Knowledge evidence must say **which** entry it is
219
+ (`case_submit_evidence(source_skill="knowledge-kb", knowledge_entry_id="KB-…")`)
220
+ — "some applicable entry is mounted somewhere" is a different claim from "this
221
+ one applies", and only the second can carry a conclusion.
222
+
223
+ Matching is **by version applicability, never by similarity** — an entry written
224
+ for the wrong build reads exactly like the right one, and similarity is the only
225
+ thing that would let it through. An entry with no `applies_to` can support a
226
+ hypothesis but can never make a case Confirmed. A constraint the case scope
227
+ cannot answer is not a match either: silence is not a pass.
228
+
182
229
  > **On a stock install the ceiling is Probable.** Confirmed needs a decisive
183
230
  > source, and there is neither a hardware-diagnostic channel (no Redfish/BMC, no
184
231
  > SMART/NVMe) nor a knowledge library mounted — `~/.vmware/knowledge/` ships
@@ -209,8 +256,10 @@ vmware-debug mcp # start stdio MCP server (proxy-
209
256
  ## Troubleshooting
210
257
 
211
258
  - **`incident_timeline` raises "event[N] could not be normalised"** — event N is missing a timestamp or has an unparseable one. Every event needs `ts` (ISO-8601, epoch seconds, or millis).
212
- - **All hypotheses come back "uncategorized"** — the symptom isn't in the catalogue yet; widen the window and pull from another source (aria anomalies, log-insight). Consider adding a signature (see `references/routing.md`).
213
- - **No spikes detected on an obvious burst** — you need ≥3 time bins for a baseline; shrink `bin_seconds`.
259
+ - **Most events come back "uncategorized"** — read `classification` in the result: it says how many, what share, and quotes the texts that matched nothing. Do **not** widen the window; a wider window adds baseline, not signal. The spikes still tell you *when*, which is answerable without a category. If the samples name a subsystem the taxonomy does not know, add a signature (see `references/routing.md`).
260
+ - **No spikes detected on an obvious burst** — check `binning` for the resolution you were given. The width is chosen from event density so that bins average ≥4 events; a burst shorter than one bin can still be flattened by it. Pass `bin_seconds` to narrow. Below three bins there is no baseline at all and nothing is reported.
261
+ - **`case_timeline` says zero events after you submitted results** — check `payload_events` in each `case_submit_evidence` reply. Events are read from a bare list, or from `items`/`events`/`rows`; a *summary* of a tool's result carries none. `note` names which items carried nothing and what keys they held instead.
262
+ - **`case_readiness` says a skill you have is not installed** — both spellings are accepted (`monitor` and `vmware-monitor`). A name it did not recognise comes back in `unrecognised_skills` rather than being read as missing.
214
263
  - **It won't execute the fix** — by design. Route to vmware-aiops or vmware-pilot.
215
264
 
216
265
  ## Audit & Safety
@@ -14,7 +14,7 @@ vmware-monitor and vmware-aria against a production vSphere estate with Llama
14
14
  cross-skill rules are identical across this family; the parts below marked
15
15
  vmware-debug are specific to this skill.
16
16
 
17
- vmware-debug exposes 13 MCP tools: 6 reads and 7 writes. It connects to nothing
17
+ vmware-debug exposes 14 MCP tools: 7 reads and 7 writes. It connects to nothing
18
18
  and holds no credentials — the calling agent gathers events from the other
19
19
  skills, normalises them, and hands them over. The four writes go to the local
20
20
  investigation ledger under `~/.vmware/cases/`, never to a VMware system. That
@@ -27,9 +27,16 @@ fixed in-process constant, so `total` is a real count and `limit` is `null`.
27
27
 
28
28
  ## Symptom categories
29
29
 
30
- `storage`, `network`, `compute`, `ha_drs`, `power_lifecycle`, `auth`, `platform`.
30
+ `storage`, `network`, `compute`, `ha_drs`, `host_lifecycle`, `power_lifecycle`,
31
+ `auth`, `platform`.
31
32
  See `references/routing.md` for keyword signatures and the skill each routes to.
32
33
 
34
+ `host_lifecycle` is a host changing its own availability state — maintenance
35
+ mode, shutdown, reboot, standby, connection loss, sync failure.
36
+ `power_lifecycle` is the VM-level equivalent. They are separate because they are
37
+ separate investigations: the second is a task question for vmware-aiops, the
38
+ first is a cluster, DPM, vLCM or drift question.
39
+
33
40
  ## Design properties
34
41
 
35
42
  - **Zero cross-skill runtime deps** — correlation is pure functions over plain
@@ -21,7 +21,7 @@ runtime dependency** on the other packages (no version lockstep, no heavy instal
21
21
  | Field | Type | Notes |
22
22
  |---|---|---|
23
23
  | `ts` | string \| number | ISO-8601, epoch **seconds**, or epoch **millis** (auto-detected). Required. |
24
- | `source` | string | `monitor` \| `aria` \| `loginsight` \| `nsx` \| `nsx-security` \| `storage` \| ... |
24
+ | `source` | string | `monitor` \| `aria` \| `loginsight` \| `nsx` \| `nsx-security` \| `storage` \| ... The catalogue in `rules/evidence_sources.yaml` spells the same skills `vmware-monitor`, `vmware-aria`, `vmware-log-insight`. Both spellings are understood wherever a skill is named — `case_readiness(available_skills=...)`, and the grader's count of independent sources, which treats two spellings of one skill as one source. |
25
25
  | `severity` | string | Free text; normalised to `critical`/`error`/`warning`/`info`/`unknown`. |
26
26
  | `entity` | string | The object the event is about (VM/host/datastore). May be empty. |
27
27
  | `text` | string | Human-readable message — this is what the symptom classifier matches on. |
@@ -10,6 +10,7 @@ Keep this in sync with `_CATEGORY_SIGNATURES` in `vmware_debug/ops/timeline.py`
10
10
  | **network** | vmotion, uplink, link down, mtu, firewall, dfw, segment, tier-0, bgp | vmware-nsx, vmware-nsx-security (traceflow, DFW hits) | pilot |
11
11
  | **compute** | cpu ready, memory, balloon, swap, contention, numa | vmware-aria (metrics + anomalies) | pilot (rightsizing) |
12
12
  | **ha_drs** | ha, high availability, drs, failover, admission control, isolation | vmware-monitor, vmware-aiops (cluster) | pilot |
13
+ | **host_lifecycle** | maintenance mode, shut down of, host reboot, standby mode, lost connection to, cannot synchronize, not responding | vmware-monitor (host/cluster state), vmware-harden (drift — a host that left service on cue was told to), vmware-log-insight (vpxd/hostd) | pilot |
13
14
  | **power_lifecycle** | power on/off, failed to start, boot, vmx, ovf, clone, snapshot | vmware-aiops (task status, snapshot tree), vmware-monitor | aiops |
14
15
  | **auth** | login, authentication, denied, 401, 403, token, certificate, tls | config/.env, target cert + time sync | config fix |
15
16
  | **platform** | vpxd, hostd, service restart, crash, 503, not responding, disconnected | vmware-monitor (connection/service), vmware-log-insight (vpxd/hostd) | pilot |
@@ -9,13 +9,14 @@
9
9
  "list_symptom_categories",
10
10
  "case_open",
11
11
  "case_list",
12
- "case_readiness"
12
+ "case_readiness",
13
+ "case_knowledge"
13
14
  ],
14
15
  "reachable_on_surface": 8,
15
16
  "reachable_only_via_description": 0,
16
17
  "required_entity_params": 8,
17
18
  "required_params_classified": "8/17",
18
- "tool_count": 13
19
+ "tool_count": 14
19
20
  },
20
21
  "maximum": 8,
21
22
  "pct": 100.0,
@@ -43,12 +44,12 @@
43
44
  },
44
45
  "entry_point_availability": {
45
46
  "detail": {
46
- "full_entry_points": 5
47
+ "full_entry_points": 6
47
48
  },
48
- "maximum": 13,
49
- "pct": 38.5,
49
+ "maximum": 14,
50
+ "pct": 42.9,
50
51
  "unit": "entry points",
51
- "value": 5
52
+ "value": 6
52
53
  },
53
54
  "error_actionability": {
54
55
  "detail": {
@@ -77,7 +78,7 @@
77
78
  "names_artifact",
78
79
  "states_remedy"
79
80
  ],
80
- "where": "evidence.py:281"
81
+ "where": "evidence.py:289"
81
82
  },
82
83
  {
83
84
  "message": "gaps.json in case {} is not valid JSON: {}. It was hand-edited. Restore it rather than deleting it \u2014",
@@ -85,7 +86,7 @@
85
86
  "names_artifact",
86
87
  "states_remedy"
87
88
  ],
88
- "where": "evidence.py:283"
89
+ "where": "evidence.py:291"
89
90
  },
90
91
  {
91
92
  "message": "Cannot read grading_rules.yaml at {}: {}",
@@ -93,7 +94,7 @@
93
94
  "names_artifact",
94
95
  "states_remedy"
95
96
  ],
96
- "where": "grading.py:55"
97
+ "where": "grading.py:57"
97
98
  },
98
99
  {
99
100
  "message": "grading_rules.yaml at {} is not valid YAML: {}. Fix it rather than deleting it \u2014 falling back to the",
@@ -101,7 +102,7 @@
101
102
  "names_artifact",
102
103
  "states_remedy"
103
104
  ],
104
- "where": "grading.py:57"
105
+ "where": "grading.py:59"
105
106
  },
106
107
  {
107
108
  "message": "A hypothesis needs a statement \u2014 what you think happened, in one line. An unnamed hypothesis cannot ",
@@ -134,7 +135,7 @@
134
135
  "names_artifact",
135
136
  "states_remedy"
136
137
  ],
137
- "where": "plan.py:72"
138
+ "where": "plan.py:84"
138
139
  },
139
140
  {
140
141
  "message": "evidence_sources.yaml at {} is missing 'classes' or 'routing'. An empty catalogue would make every r",
@@ -166,7 +167,7 @@
166
167
  "names_artifact",
167
168
  "states_remedy"
168
169
  ],
169
- "where": "timeline.py:140"
170
+ "where": "timeline.py:137"
170
171
  },
171
172
  {
172
173
  "message": "Cannot read case.json for {}: {}",
@@ -174,7 +175,7 @@
174
175
  "names_artifact",
175
176
  "states_remedy"
176
177
  ],
177
- "where": "timeline.py:154"
178
+ "where": "timeline.py:151"
178
179
  }
179
180
  ],
180
181
  "per_dimension_pct": {
@@ -193,20 +194,20 @@
193
194
  "detail": {
194
195
  "heaviest_tools": {
195
196
  "case_open": 354,
196
- "case_plan": 360,
197
+ "case_plan": 395,
197
198
  "case_record_gap": 337,
198
- "case_submit_evidence": 549,
199
- "incident_timeline": 329
199
+ "case_submit_evidence": 693,
200
+ "incident_timeline": 357
200
201
  },
201
- "manifest_tokens": 3428,
202
- "mean_tokens_per_tool": 263.7,
202
+ "manifest_tokens": 3988,
203
+ "mean_tokens_per_tool": 284.9,
203
204
  "reference_context": 16384,
204
- "tool_count": 13
205
+ "tool_count": 14
205
206
  },
206
207
  "maximum": 16384,
207
- "pct": 79.1,
208
+ "pct": 75.7,
208
209
  "unit": "tokens",
209
- "value": 12956
210
+ "value": 12396
210
211
  },
211
212
  "parameter_documentation_coverage": {
212
213
  "detail": {
@@ -215,8 +216,7 @@
215
216
  "case_id"
216
217
  ],
217
218
  "case_submit_evidence": [
218
- "fetched_at",
219
- "summary"
219
+ "fetched_at"
220
220
  ],
221
221
  "case_timeline": [
222
222
  "bin_seconds",
@@ -226,22 +226,23 @@
226
226
  ]
227
227
  }
228
228
  },
229
- "maximum": 43,
230
- "pct": 83.7,
229
+ "maximum": 45,
230
+ "pct": 86.7,
231
231
  "unit": "parameters",
232
- "value": 36
232
+ "value": 39
233
233
  },
234
234
  "per_tool_token_discipline": {
235
235
  "detail": {
236
236
  "ceiling": 350,
237
237
  "over_ceiling": {
238
238
  "case_open": 354,
239
- "case_plan": 360,
240
- "case_submit_evidence": 549
239
+ "case_plan": 395,
240
+ "case_submit_evidence": 693,
241
+ "incident_timeline": 357
241
242
  }
242
243
  },
243
- "maximum": 13,
244
- "pct": 76.9,
244
+ "maximum": 14,
245
+ "pct": 71.4,
245
246
  "unit": "tools",
246
247
  "value": 10
247
248
  },
@@ -263,10 +264,10 @@
263
264
  "detail": {
264
265
  "unmarked": []
265
266
  },
266
- "maximum": 13,
267
+ "maximum": 14,
267
268
  "pct": 100.0,
268
269
  "unit": "points",
269
- "value": 13
270
+ "value": 14
270
271
  },
271
272
  "remedy_survives_truncation": {
272
273
  "detail": {
@@ -277,10 +278,10 @@
277
278
  "envelope.py:138",
278
279
  "conclusion.py:135",
279
280
  "conclusion.py:144",
280
- "evidence.py:172",
281
- "evidence.py:204",
282
- "evidence.py:240",
283
- "grading.py:65",
281
+ "evidence.py:179",
282
+ "evidence.py:211",
283
+ "evidence.py:248",
284
+ "grading.py:67",
284
285
  "hypotheses.py:47",
285
286
  "hypotheses.py:70"
286
287
  ]
@@ -300,14 +301,14 @@
300
301
  "tool_description_quality": {
301
302
  "detail": {
302
303
  "per_dimension_pct": {
303
- "args": 76.9,
304
- "gotcha": 84.6,
304
+ "args": 78.6,
305
+ "gotcha": 85.7,
305
306
  "marker": 100.0,
306
- "next_hop": 61.5,
307
+ "next_hop": 57.1,
307
308
  "what": 100.0,
308
- "when": 92.3
309
+ "when": 92.9
309
310
  },
310
- "tools_graded": 13,
311
+ "tools_graded": 14,
311
312
  "weakest_tools": {
312
313
  "case_close": [
313
314
  "next_hop"
@@ -319,6 +320,9 @@
319
320
  "args",
320
321
  "next_hop"
321
322
  ],
323
+ "case_knowledge": [
324
+ "next_hop"
325
+ ],
322
326
  "case_list": [
323
327
  "gotcha",
324
328
  "next_hop"
@@ -332,29 +336,26 @@
332
336
  ],
333
337
  "case_submit_evidence": [
334
338
  "args"
335
- ],
336
- "case_timeline": [
337
- "args"
338
339
  ]
339
340
  }
340
341
  },
341
- "maximum": 78,
342
- "pct": 85.9,
342
+ "maximum": 84,
343
+ "pct": 85.7,
343
344
  "unit": "points",
344
- "value": 67
345
+ "value": 72
345
346
  },
346
347
  "tool_failure_payload_quality": {
347
348
  "detail": {
348
349
  "bare_string_error_lines": [],
349
- "carries_hint": 12,
350
- "dict_shaped": 12,
351
- "error_return_sites": 12,
352
- "hint_names_artifact": 12
350
+ "carries_hint": 13,
351
+ "dict_shaped": 13,
352
+ "error_return_sites": 13,
353
+ "hint_names_artifact": 13
353
354
  },
354
- "maximum": 36,
355
+ "maximum": 39,
355
356
  "pct": 100.0,
356
357
  "unit": "checks",
357
- "value": 36
358
+ "value": 39
358
359
  }
359
360
  }
360
361
  }
@@ -0,0 +1,86 @@
1
+ """A case opened in Chinese got no identity of its own.
2
+
3
+ ``slugify`` reduces on ``[^a-z0-9]+``, so a summary written entirely in Chinese
4
+ leaves nothing behind and the id falls back to the literal word ``case``:
5
+ ``20260830-061014-case``. Two completely different investigations opened in the
6
+ same second then collide — and the collision message said they were "the same
7
+ summary", which was false, and sent the reporter looking for a duplicate that
8
+ did not exist.
9
+
10
+ The id has to stay inside its character set: it is a directory name, and
11
+ ``validate_case_id`` is the only thing between a model-chosen string and
12
+ ``open(root / case_id)``. So the fix is not to widen the alphabet. It is that an
13
+ id must distinguish summaries that differ, and that the collision message must
14
+ describe what actually collided.
15
+
16
+ The control is readability: an ASCII summary must still produce the readable
17
+ slug the listing was designed around, not a digest for everybody.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import pytest
23
+
24
+ from vmware_debug.ops.cases.ids import new_case_id, validate_case_id
25
+ from vmware_debug.ops.cases.model import Scope
26
+ from vmware_debug.ops.cases.store import CaseExists, create_case
27
+
28
+ AT = "2026-08-30T06:10:14Z"
29
+
30
+ #: Both must be free of Latin characters. An earlier draft wrote the first one
31
+ #: as "四台 ESXi 主机…", and "esxi" survived slugify — so the two ids differed
32
+ #: whatever the fallback did, and the test passed against the defect it was
33
+ #: written to catch.
34
+ ZH_A = "四台主机同时进入维护模式后关机"
35
+ ZH_B = "存储集群延迟升高导致虚拟机无响应"
36
+
37
+
38
+ @pytest.fixture(autouse=True)
39
+ def isolated_home(tmp_path, monkeypatch):
40
+ monkeypatch.setenv("OPS_HOME", str(tmp_path / "vmware"))
41
+
42
+
43
+ class TestANonAsciiSummaryStillGetsAnIdentity:
44
+ def test_two_different_chinese_summaries_do_not_collide(self):
45
+ assert new_case_id(ZH_A, AT) != new_case_id(ZH_B, AT)
46
+
47
+ def test_the_id_is_still_a_safe_directory_name(self):
48
+ validate_case_id(new_case_id(ZH_A, AT))
49
+
50
+ def test_the_same_summary_still_yields_the_same_id(self):
51
+ """Ids are replayable: a case folder re-opened from its scope has to
52
+ reconstruct the id it already has."""
53
+ assert new_case_id(ZH_A, AT) == new_case_id(ZH_A, AT)
54
+
55
+ def test_both_cases_can_actually_be_opened_in_the_same_second(self):
56
+ create_case(Scope(summary=ZH_A, determined_by="user report"), at=AT)
57
+ create_case(Scope(summary=ZH_B, determined_by="user report"), at=AT)
58
+
59
+
60
+ class TestTheCollisionMessageIsTrue:
61
+ def test_it_does_not_claim_the_summaries_were_identical(self):
62
+ """Two summaries that differ only outside the id character set still
63
+ collide, and telling the reporter they submitted the same summary sends
64
+ them looking for something that is not there."""
65
+ create_case(Scope(summary="vsan latency!", determined_by="a"), at=AT)
66
+ with pytest.raises(CaseExists) as exc:
67
+ create_case(Scope(summary="vsan latency?", determined_by="a"), at=AT)
68
+ assert "same summary" not in str(exc.value)
69
+
70
+ def test_it_says_what_to_do_about_it(self):
71
+ create_case(Scope(summary="vsan latency", determined_by="a"), at=AT)
72
+ with pytest.raises(CaseExists) as exc:
73
+ create_case(Scope(summary="vsan latency", determined_by="a"), at=AT)
74
+ assert "case_get" in str(exc.value)
75
+
76
+
77
+ class TestControlReadabilityIsNotSacrificed:
78
+ def test_an_ascii_summary_still_gets_its_readable_slug(self):
79
+ """A digest for every case would pass every test above and destroy the
80
+ one property the slug exists for — a directory listing you can read."""
81
+ assert new_case_id("vSAN datastore latency on cluster-01", AT).endswith(
82
+ "-vsan-datastore-latency-on-cluster-01"
83
+ )
84
+
85
+ def test_a_mixed_summary_keeps_the_part_that_survives(self):
86
+ assert "esxi05" in new_case_id("主机 esxi05 关机", AT)