vmware-debug 1.9.0__tar.gz → 1.10.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 (74) hide show
  1. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/.claude-plugin/plugin.json +1 -1
  2. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/.mcp.json +1 -1
  3. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/PKG-INFO +1 -1
  4. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/RELEASE_NOTES.md +42 -0
  5. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/pyproject.toml +1 -1
  6. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/server.json +2 -2
  7. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/skills/vmware-debug/SKILL.md +48 -1
  8. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/skills/vmware-debug/references/agent-guardrails.md +1 -1
  9. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/capability/_scores.json +47 -46
  10. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/regression/test_investigation_metrics.py +103 -4
  11. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/test_case_grading.py +29 -4
  12. vmware_debug-1.10.0/tests/test_knowledge.py +237 -0
  13. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/__init__.py +1 -1
  14. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/mcp/tools.py +2 -0
  15. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/mcp_server/server.py +41 -1
  16. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/cases/api.py +35 -0
  17. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/cases/evidence.py +8 -0
  18. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/cases/grading.py +73 -0
  19. vmware_debug-1.10.0/vmware_debug/ops/cases/knowledge.py +375 -0
  20. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/.gitignore +0 -0
  21. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/README-CN.md +0 -0
  22. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/README.md +0 -0
  23. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/SECURITY.md +0 -0
  24. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/docs/plans/2026-08-28-investigation-layer-design.md +0 -0
  25. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/skills/vmware-debug/references/capabilities.md +0 -0
  26. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/skills/vmware-debug/references/cli-reference.md +0 -0
  27. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/skills/vmware-debug/references/event-envelope.md +0 -0
  28. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/skills/vmware-debug/references/routing.md +0 -0
  29. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/skills/vmware-debug/references/setup-guide.md +0 -0
  30. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/__init__.py +0 -0
  31. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/capability/__init__.py +0 -0
  32. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/capability/_family.py +0 -0
  33. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/capability/_scoring.py +0 -0
  34. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/capability/_skill.py +0 -0
  35. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/capability/conftest.py +0 -0
  36. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/capability/test_entity_reachability.py +0 -0
  37. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/capability/test_error_actionability.py +0 -0
  38. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/capability/test_tool_description_quality.py +0 -0
  39. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
  40. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/regression/__init__.py +0 -0
  41. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/regression/test_capability_grader.py +0 -0
  42. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/regression/test_debug_regressions.py +0 -0
  43. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/regression/test_result_envelope.py +0 -0
  44. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/eval/regression/test_tool_annotations.py +0 -0
  45. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/test_case_conclusion.py +0 -0
  46. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/test_case_evidence.py +0 -0
  47. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/test_case_hypotheses.py +0 -0
  48. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/test_case_ids.py +0 -0
  49. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/test_case_plan.py +0 -0
  50. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/test_case_readiness.py +0 -0
  51. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/test_case_store.py +0 -0
  52. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/test_case_timeline_close.py +0 -0
  53. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/test_case_tools.py +0 -0
  54. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/test_safe_error_passthrough.py +0 -0
  55. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/tests/test_timeline.py +0 -0
  56. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/uv.lock +0 -0
  57. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/cli.py +0 -0
  58. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/envelope.py +0 -0
  59. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/mcp/__init__.py +0 -0
  60. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/mcp_server/__init__.py +0 -0
  61. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/__init__.py +0 -0
  62. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/cases/__init__.py +0 -0
  63. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/cases/conclusion.py +0 -0
  64. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/cases/hypotheses.py +0 -0
  65. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/cases/ids.py +0 -0
  66. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/cases/model.py +0 -0
  67. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/cases/plan.py +0 -0
  68. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/cases/readiness.py +0 -0
  69. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/cases/sources.py +0 -0
  70. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/cases/store.py +0 -0
  71. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/cases/timeline.py +0 -0
  72. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/ops/timeline.py +0 -0
  73. {vmware_debug-1.9.0 → vmware_debug-1.10.0}/vmware_debug/rules/evidence_sources.yaml +0 -0
  74. {vmware_debug-1.9.0 → vmware_debug-1.10.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.10.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.10.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.10.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,45 @@
1
+ ## v1.10.0 — the knowledge layer is implemented, not just described
2
+
3
+ v1.9.0 shipped the investigation layer with `applies_to` documented as the thing
4
+ that makes a knowledge entry decisive. Nothing parsed a knowledge file and
5
+ nothing checked applicability. Any file at all under the knowledge root raised
6
+ the reported ceiling, and anything labelled `knowledge-kb` counted as decisive.
7
+ The route to a wrong Confirmed that the design was written to close was open the
8
+ whole time, and the metric said zero only because no fixture walked it.
9
+
10
+ `case_knowledge` (the 14th tool) answers the first question anyone mounting a
11
+ library asks — which formats are taken — without them reading a design document:
12
+
13
+ .md .markdown YAML front-matter between --- fences, body below (preferred)
14
+ .yaml .yml the whole file is one entry
15
+ .json one entry per file
16
+ .jsonl one JSON object per line (what ticketing systems export)
17
+ .csv .tsv one entry per row; dotted columns for nested constraints
18
+ .txt .log a sibling <name>.yaml carries the metadata
19
+
20
+ PDF, DOCX, PPTX and HTML are named as needing conversion rather than ignored,
21
+ and a file that cannot be parsed is reported rather than skipped.
22
+
23
+ **An entry is decisive only if its own `applies_to` was checked against the
24
+ case's recorded versions and passed.** Three things follow, each found by
25
+ reviewing the version before it:
26
+
27
+ * Knowledge evidence must say WHICH entry it is. "Some applicable entry is
28
+ mounted somewhere" is a different claim from "this one applies", and the first
29
+ version accepted it — an agent citing the entry written for 9.x could confirm
30
+ a case about 8.0.3 as long as an unrelated entry happened to match.
31
+ * A constraint the case scope cannot answer is not a match. Silence is not a
32
+ pass; reading it as one is how an entry for different hardware ends a case.
33
+ * **A constraint the checker cannot evaluate is not a pass either.** The first
34
+ version understood product, build, driver and firmware and treated every other
35
+ key as satisfied, so `build` without a `product`, a `hardware_model` list, and
36
+ an outright typo all came back decisive. Unknown and satisfied must never be
37
+ the same answer.
38
+
39
+ Given two entries that read identically — one for `>=8.0, <9.0`, one for
40
+ `>=9.0` — a case scoped to vSphere 8.0.3 now reaches Confirmed citing the first
41
+ and stays at Probable citing the second, naming the range that excluded it.
42
+
1
43
  ## v1.9.0 — the investigation layer: 2 tools become 13
2
44
 
3
45
  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.10.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,12 @@
7
7
  "url": "https://github.com/vmware-skills/VMware-Debug",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.9.0",
10
+ "version": "1.10.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "vmware-debug",
15
- "version": "1.9.0",
15
+ "version": "1.10.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
@@ -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
@@ -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
@@ -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:56"
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:58"
105
106
  },
106
107
  {
107
108
  "message": "A hypothesis needs a statement \u2014 what you think happened, in one line. An unnamed hypothesis cannot ",
@@ -195,18 +196,18 @@
195
196
  "case_open": 354,
196
197
  "case_plan": 360,
197
198
  "case_record_gap": 337,
198
- "case_submit_evidence": 549,
199
+ "case_submit_evidence": 621,
199
200
  "incident_timeline": 329
200
201
  },
201
- "manifest_tokens": 3428,
202
- "mean_tokens_per_tool": 263.7,
202
+ "manifest_tokens": 3777,
203
+ "mean_tokens_per_tool": 269.8,
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": 76.9,
208
209
  "unit": "tokens",
209
- "value": 12956
210
+ "value": 12607
210
211
  },
211
212
  "parameter_documentation_coverage": {
212
213
  "detail": {
@@ -226,10 +227,10 @@
226
227
  ]
227
228
  }
228
229
  },
229
- "maximum": 43,
230
- "pct": 83.7,
230
+ "maximum": 45,
231
+ "pct": 84.4,
231
232
  "unit": "parameters",
232
- "value": 36
233
+ "value": 38
233
234
  },
234
235
  "per_tool_token_discipline": {
235
236
  "detail": {
@@ -237,13 +238,13 @@
237
238
  "over_ceiling": {
238
239
  "case_open": 354,
239
240
  "case_plan": 360,
240
- "case_submit_evidence": 549
241
+ "case_submit_evidence": 621
241
242
  }
242
243
  },
243
- "maximum": 13,
244
- "pct": 76.9,
244
+ "maximum": 14,
245
+ "pct": 78.6,
245
246
  "unit": "tools",
246
- "value": 10
247
+ "value": 11
247
248
  },
248
249
  "read_only_manifest_saving": {
249
250
  "detail": {
@@ -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:66",
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
  }
@@ -114,12 +114,17 @@ def test_wrong_confirmed_rate_is_zero(label, sources, gaps, _unused):
114
114
  assert grade != "confirmed", f"{label!r} reached Confirmed — grade was {grade}"
115
115
 
116
116
 
117
- def test_the_only_route_to_confirmed_is_the_intended_one():
117
+ def test_the_only_route_to_confirmed_is_the_intended_one(tmp_path):
118
118
  """The counterpart. A metric that can only fail is not measuring anything —
119
- if nothing can reach Confirmed, 'wrong-Confirmed rate is zero' is vacuous."""
120
- cid = _case()
119
+ if nothing can reach Confirmed, 'wrong-Confirmed rate is zero' is vacuous.
120
+
121
+ Reaching it now takes a mounted entry whose applies_to matches, which is
122
+ exactly the bar the design sets.
123
+ """
124
+ _kb(tmp_path, "KB-ok.md", "applies_to:\n product: vsphere\n")
125
+ cid = _scoped_case({"vsphere": "8.0.3"})
121
126
  _ev(cid, "vmware-monitor")
122
- _ev(cid, "knowledge-sr")
127
+ _ev(cid, "knowledge-sr", knowledge_entry_id="KB-ok.md")
123
128
  assert grade_case(cid).grade == "confirmed"
124
129
 
125
130
 
@@ -215,3 +220,97 @@ def test_every_unreachable_source_carries_a_remedy():
215
220
  cid = _case()
216
221
  for u in plan_next(cid, category=category)["unavailable"]:
217
222
  assert u["how_to_supply"], f"{category}: {u['evidence_class']} has no remedy"
223
+
224
+
225
+ # ── Metric 1, continued: the route the design says must not exist ──────────
226
+ #
227
+ # Added after the knowledge layer was implemented. Until then `applies_to` was
228
+ # prose: any file at all raised the ceiling and anything labelled `knowledge-kb`
229
+ # counted as decisive. The wrong-Confirmed metric passed only because no fixture
230
+ # exercised the one route a wrong Confirmed actually takes.
231
+
232
+
233
+ def _kb(tmp_path, name, applies_block):
234
+ p = tmp_path / "vmware" / "knowledge" / "kb" / name
235
+ p.parent.mkdir(parents=True, exist_ok=True)
236
+ p.write_text(f"---\nid: {name}\n{applies_block}---\nbody\n")
237
+
238
+
239
+ def _scoped_case(versions):
240
+ cid = create_case(
241
+ Scope(
242
+ summary=f"scoped {next(_SEQ)}",
243
+ determined_by="test",
244
+ product_versions=versions,
245
+ ),
246
+ at=AT,
247
+ ).case_id
248
+ add_hypothesis(cid, "failing device")
249
+ return cid
250
+
251
+
252
+ def test_a_knowledge_entry_for_the_wrong_build_cannot_confirm(tmp_path):
253
+ """The canonical wrong Confirmed. The entry is real, well-formed, and about
254
+ the right product — and it is for 9.x while this estate runs 8.0.3."""
255
+ _kb(tmp_path, "KB-1.md", "applies_to:\n product: vsphere\n build: '>=9.0'\n")
256
+ cid = _scoped_case({"vsphere": "8.0.3"})
257
+ _ev(cid, "vmware-monitor")
258
+ _ev(cid, "knowledge-kb")
259
+ assert grade_case(cid).grade != "confirmed"
260
+
261
+
262
+ def test_a_knowledge_entry_with_no_applies_to_cannot_confirm(tmp_path):
263
+ _kb(tmp_path, "KB-2.md", "")
264
+ cid = _scoped_case({"vsphere": "8.0.3"})
265
+ _ev(cid, "vmware-monitor")
266
+ _ev(cid, "knowledge-kb")
267
+ assert grade_case(cid).grade != "confirmed"
268
+
269
+
270
+ def test_a_matching_entry_does_confirm(tmp_path):
271
+ """The counterpart, so the rule above is a filter and not a wall."""
272
+ _kb(tmp_path, "KB-3.md", "applies_to:\n product: vsphere\n build: '>=8.0, <9.0'\n")
273
+ cid = _scoped_case({"vsphere": "8.0.3"})
274
+ _ev(cid, "vmware-monitor")
275
+ _ev(cid, "knowledge-kb", knowledge_entry_id="KB-3.md")
276
+ assert grade_case(cid).grade == "confirmed"
277
+
278
+
279
+ def test_citing_the_wrong_entry_does_not_borrow_a_matching_one(tmp_path):
280
+ """Caught reviewing the applicability filter.
281
+
282
+ The first version passed when ANY mounted entry applied, so an agent citing
283
+ the entry written for 9.x could confirm a case about 8.0.3 as long as some
284
+ unrelated entry happened to match. "An entry is decisive only if ITS
285
+ applies_to passed" was true of the docstring and not of the code.
286
+
287
+ Knowledge evidence now has to say which entry it is, and that entry is the
288
+ one checked.
289
+ """
290
+ _kb(tmp_path, "KB-right.md", "applies_to:\n product: vsphere\n build: '>=8.0, <9.0'\n")
291
+ _kb(tmp_path, "KB-wrong.md", "applies_to:\n product: vsphere\n build: '>=9.0'\n")
292
+ cid = _scoped_case({"vsphere": "8.0.3"})
293
+ _ev(cid, "vmware-monitor")
294
+ _ev(cid, "knowledge-kb", knowledge_entry_id="KB-wrong.md")
295
+ assert grade_case(cid).grade != "confirmed"
296
+
297
+
298
+ def test_citing_the_right_entry_confirms(tmp_path):
299
+ _kb(tmp_path, "KB-right.md", "applies_to:\n product: vsphere\n build: '>=8.0, <9.0'\n")
300
+ _kb(tmp_path, "KB-wrong.md", "applies_to:\n product: vsphere\n build: '>=9.0'\n")
301
+ cid = _scoped_case({"vsphere": "8.0.3"})
302
+ _ev(cid, "vmware-monitor")
303
+ _ev(cid, "knowledge-kb", knowledge_entry_id="KB-right.md")
304
+ assert grade_case(cid).grade == "confirmed"
305
+
306
+
307
+ def test_knowledge_evidence_that_names_no_entry_is_not_decisive(tmp_path):
308
+ """Without the id there is nothing to check, and 'something applicable
309
+ exists somewhere' is not the same claim."""
310
+ _kb(tmp_path, "KB-right.md", "applies_to:\n product: vsphere\n---\n")
311
+ cid = _scoped_case({"vsphere": "8.0.3"})
312
+ _ev(cid, "vmware-monitor")
313
+ _ev(cid, "knowledge-kb")
314
+ r = grade_case(cid)
315
+ assert r.grade != "confirmed"
316
+ assert any("which entry" in x for x in r.reasons), r.reasons
@@ -31,6 +31,21 @@ def isolated_home(tmp_path, monkeypatch):
31
31
  monkeypatch.setenv("OPS_HOME", str(tmp_path / "vmware"))
32
32
 
33
33
 
34
+ @pytest.fixture
35
+ def mounted_kb(tmp_path):
36
+ """A knowledge entry that genuinely applies to the case fixture's scope.
37
+
38
+ Submitting evidence labelled `knowledge-sr` used to be enough to confirm a
39
+ case; the label did all the work. Now the entry has to exist and its
40
+ applies_to has to match, so a test that wants Confirmed has to construct one
41
+ — which is the point.
42
+ """
43
+ p = tmp_path / "vmware" / "knowledge" / "kb" / "KB-fixture.md"
44
+ p.parent.mkdir(parents=True, exist_ok=True)
45
+ p.write_text("---\nid: KB-fixture\napplies_to:\n product: vsphere\n---\nbody\n")
46
+ return p
47
+
48
+
34
49
  @pytest.fixture
35
50
  def case():
36
51
  """A case with H1 already registered.
@@ -39,12 +54,22 @@ def case():
39
54
  unregistered hypothesis is now refused — a dangling id used to block and
40
55
  falsify nothing while silently costing a grade level.
41
56
  """
42
- cid = create_case(Scope(summary="vsan latency", determined_by="alarm 42"), at=AT).case_id
57
+ cid = create_case(
58
+ Scope(
59
+ summary="vsan latency", determined_by="alarm 42",
60
+ product_versions={"vsphere": "8.0.3"},
61
+ ),
62
+ at=AT,
63
+ ).case_id
43
64
  add_hypothesis(cid, "failing device")
44
65
  return cid
45
66
 
46
67
 
47
68
  def evidence(skill="vmware-monitor", tool="list_events", **kw):
69
+ # Knowledge evidence must say which entry it is; without that its
70
+ # applicability cannot be checked and it is not decisive.
71
+ if skill in ("knowledge-kb", "knowledge-sr") and "knowledge_entry_id" not in kw:
72
+ kw["knowledge_entry_id"] = "KB-fixture"
48
73
  return Evidence(
49
74
  source_skill=skill,
50
75
  source_tool=tool,
@@ -131,7 +156,7 @@ class TestConfirmedNeedsADecisiveSource:
131
156
  record_evidence(case, evidence(skill=skill))
132
157
  assert grade_case(case).grade == "probable"
133
158
 
134
- def test_a_decisive_source_reaches_confirmed(self, case):
159
+ def test_a_decisive_source_reaches_confirmed(self, case, mounted_kb):
135
160
  record_evidence(case, evidence(skill="vmware-monitor"))
136
161
  record_evidence(case, evidence(skill="knowledge-sr"))
137
162
  assert grade_case(case).grade == "confirmed"
@@ -178,7 +203,7 @@ class TestExclusionNeedsAFalsifyingObservation:
178
203
 
179
204
 
180
205
  class TestDemotion:
181
- def test_a_new_gap_takes_confirmed_back_down_to_probable(self, case):
206
+ def test_a_new_gap_takes_confirmed_back_down_to_probable(self, case, mounted_kb):
182
207
  record_evidence(case, evidence(skill="vmware-monitor"))
183
208
  record_evidence(case, evidence(skill="knowledge-sr"))
184
209
  assert grade_case(case).grade == "confirmed"
@@ -265,7 +290,7 @@ class TestTheRulesFileHasNoDecorativeKnobs:
265
290
  p.parent.mkdir(parents=True, exist_ok=True)
266
291
  p.write_text(body)
267
292
 
268
- def test_relaxing_the_prerequisite_actually_relaxes_it(self, case, tmp_path):
293
+ def test_relaxing_the_prerequisite_actually_relaxes_it(self, case, tmp_path, mounted_kb):
269
294
  """One decisive source, no corroboration: Confirmed under the relaxed
270
295
  rule, and not under the packaged one."""
271
296
  record_evidence(case, evidence(skill="knowledge-sr"))