katalyst-plugin-context 0.2.1a5__tar.gz → 0.2.1a7__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 (47) hide show
  1. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/PKG-INFO +1 -1
  2. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/pyproject.toml +1 -1
  3. katalyst_plugin_context-0.2.1a7/xspec/.gitignore +8 -0
  4. katalyst_plugin_context-0.2.1a7/xspec/features/cli/52_kata_context.feature +274 -0
  5. katalyst_plugin_context-0.2.1a7/xspec/features/cli/53_kata_context_budget_policy.feature +265 -0
  6. katalyst_plugin_context-0.2.1a7/xspec/features/mcp/09_mcp_context.feature +190 -0
  7. katalyst_plugin_context-0.2.1a7/xspec/fixtures/context-catalogue/catalogue/areas/payments.yaml +13 -0
  8. katalyst_plugin_context-0.2.1a7/xspec/fixtures/context-catalogue/catalogue/checks/encrypt-card-data.scan-for-card-numbers.yaml +12 -0
  9. katalyst_plugin_context-0.2.1a7/xspec/fixtures/context-catalogue/catalogue/components/payments.checkout.web-api.yaml +15 -0
  10. katalyst_plugin_context-0.2.1a7/xspec/fixtures/context-catalogue/catalogue/decisions/async-events.yaml +10 -0
  11. katalyst_plugin_context-0.2.1a7/xspec/fixtures/context-catalogue/catalogue/decisions/pin-httpx.yaml +10 -0
  12. katalyst_plugin_context-0.2.1a7/xspec/fixtures/context-catalogue/catalogue/decisions/use-postgres.yaml +10 -0
  13. katalyst_plugin_context-0.2.1a7/xspec/fixtures/context-catalogue/catalogue/packages/httpx.yaml +11 -0
  14. katalyst_plugin_context-0.2.1a7/xspec/fixtures/context-catalogue/catalogue/rules/encrypt-card-data.yaml +10 -0
  15. katalyst_plugin_context-0.2.1a7/xspec/fixtures/context-catalogue/catalogue/rules/log-correlation-id.yaml +10 -0
  16. katalyst_plugin_context-0.2.1a7/xspec/fixtures/context-catalogue/catalogue/services/payments.checkout.yaml +13 -0
  17. katalyst_plugin_context-0.2.1a7/xspec/package.json +15 -0
  18. katalyst_plugin_context-0.2.1a7/xspec/playwright.config.ts +5 -0
  19. katalyst_plugin_context-0.2.1a7/xspec/steps/context.ts +216 -0
  20. katalyst_plugin_context-0.2.1a7/xspec/steps/steps.ts +5 -0
  21. katalyst_plugin_context-0.2.1a7/xspec/tsconfig.json +4 -0
  22. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/.gitignore +0 -0
  23. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/__init__.py +0 -0
  24. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/adapters/__init__.py +0 -0
  25. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/adapters/runtime_graph.py +0 -0
  26. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/cli.py +0 -0
  27. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/composition.py +0 -0
  28. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/domain/__init__.py +0 -0
  29. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/domain/context.py +0 -0
  30. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/domain/graph.py +0 -0
  31. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/domain/markdown.py +0 -0
  32. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/domain/paths.py +0 -0
  33. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/domain/policy.py +0 -0
  34. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/domain/settings.py +0 -0
  35. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/lint.py +0 -0
  36. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/mcp_tools.py +0 -0
  37. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/plugin.py +0 -0
  38. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/py.typed +0 -0
  39. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/rest.py +0 -0
  40. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/schema.py +0 -0
  41. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/src/katalyst_plugin_context/service.py +0 -0
  42. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/tests/conftest.py +0 -0
  43. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/tests/context_support.py +0 -0
  44. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/tests/test_context.py +0 -0
  45. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/tests/test_lint.py +0 -0
  46. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/tests/test_shared_names.py +0 -0
  47. {katalyst_plugin_context-0.2.1a5 → katalyst_plugin_context-0.2.1a7}/tests/test_surfaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: katalyst-plugin-context
3
- Version: 0.2.1a5
3
+ Version: 0.2.1a7
4
4
  Summary: Katalyst context plugin — the right context from the taxonomy for an agent working on a node
5
5
  Author: eSimplicity
6
6
  License: Proprietary
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "katalyst-plugin-context"
3
- version = "0.2.1a5"
3
+ version = "0.2.1a7"
4
4
  description = "Katalyst context plugin — the right context from the taxonomy for an agent working on a node"
5
5
  license = { text = "Proprietary" }
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ .features-gen
3
+ playwright-report
4
+ test-results
5
+ cucumber-report
6
+ storage
7
+ .env
8
+ blob-report
@@ -0,0 +1,274 @@
1
+ @cli @area-context
2
+ Feature: Kata CLI - Context: what an agent working on a node gets
3
+ As an agent about to change a node
4
+ I want one call that returns the decisions and rules around it
5
+ So that I meet a rule the node inherits from a parent, not only the ones in its own file
6
+
7
+ # docs/plugins/context/index.md, against the neutral service catalogue of examples/catalogue
8
+ # (installed as docs/user-guide/getting-started.md installs it) with a small payments model:
9
+ #
10
+ # area payments governed by use-postgres, follows the mandatory rule encrypt-card-data
11
+ # service payments.checkout governed by async-events
12
+ # component payments.checkout.web-api follows the recommended rule log-correlation-id,
13
+ # uses the package httpx (governed by pin-httpx)
14
+ # check scan-for-card-numbers, under encrypt-card-data
15
+ #
16
+ # The catalogue declares one view, `governance`, that every `component` always includes, and
17
+ # a second, `package-decisions`, that the caller must ask for. `severity: mandatory` blocks.
18
+
19
+ Background:
20
+ Given the payments catalogue is installed
21
+
22
+ # ─────────────────────────────────────────────────────────────
23
+ # The map
24
+ # ─────────────────────────────────────────────────────────────
25
+
26
+ @smoke
27
+ Scenario: The map shows the ancestors, the relationships, the views and what can be asked next
28
+ When I run "kata tax context filesystem:payments.checkout.web-api --map"
29
+ Then the command should succeed
30
+ And the output should contain "# Context: component `filesystem:payments.checkout.web-api`"
31
+ And the output should contain "Ancestors: service `filesystem:payments.checkout` ← area `filesystem:payments`"
32
+ And the output should contain "- `USES_PACKAGE:out` → package ×1"
33
+ And the output should contain "- `GOVERNED_BY:out` → decision ×1 — inherited from `filesystem:payments.checkout`"
34
+ And the output should contain "- `FOLLOWS_RULE:out` → rule ×1 — inherited from `filesystem:payments`"
35
+ And the output should contain "- **governance** (always included): Governing decisions and rules — 5 item(s)"
36
+ And the output should contain "- **package-decisions**: Decisions on the packages it uses — 1 item(s)"
37
+ And the output should contain "## Always included"
38
+
39
+ Scenario: The map in JSON marks the always-included view and says what policy allows
40
+ When I run "kata tax context filesystem:payments.checkout.web-api --map --format json"
41
+ Then the command should succeed
42
+ And the JSON output at "$.node.qualifiedName" should equal "filesystem:payments.checkout.web-api"
43
+ And the JSON output at "$.ancestors[*].qualifiedName" should contain "filesystem:payments.checkout"
44
+ And the JSON output at "$.ancestors[*].qualifiedName" should contain "filesystem:payments"
45
+ And the JSON output at "$.views[?(@.name=='governance')].always" should equal "true"
46
+ And the JSON output at "$.views[?(@.name=='package-decisions')].always" should equal "false"
47
+ And the JSON output at "$.always.items[*]" should have 5 items
48
+ And the JSON output at "$.options.steps" should contain "USES_PACKAGE:out"
49
+ And the JSON output at "$.options.detail" should contain "full"
50
+ And the JSON output at "$.options.maxDepth" should equal "4"
51
+
52
+ # ─────────────────────────────────────────────────────────────
53
+ # A default call: the always-included view, blocking items first
54
+ # ─────────────────────────────────────────────────────────────
55
+
56
+ @smoke
57
+ Scenario: A call with no path and no view returns the always-included view, the mandatory rule first
58
+ When I run "kata tax context filesystem:payments.checkout.web-api"
59
+ Then the command should succeed
60
+ And the output should contain "## Governing decisions and rules"
61
+ And the output should contain "⛔ blocking · **Encrypt card data** `filesystem:encrypt-card-data` · severity: mandatory"
62
+ And the output should contain "_advisory_ · **Log a correlation id** `filesystem:log-correlation-id` · severity: recommended"
63
+ And the output should contain "why: area filesystem:payments ─FOLLOWS_RULE→ (inherited)"
64
+ And the output should list "Encrypt card data" before "Log a correlation id"
65
+ And the output should list "Encrypt card data" before "Publish events, do not call"
66
+ And the output should not contain "Pin httpx"
67
+ And the output should contain "nothing narrowed"
68
+
69
+ Scenario: The same call in JSON marks the blocking item and where it was inherited from
70
+ When I run "kata tax context filesystem:payments.checkout.web-api --format json"
71
+ Then the command should succeed
72
+ And the JSON output at "$.items[*]" should have 5 items
73
+ And the JSON output at "$.items[0].qualifiedName" should equal "filesystem:encrypt-card-data"
74
+ And the JSON output at "$.items[0].blocking.blocking" should equal "true"
75
+ And the JSON output at "$.items[0].inheritedFrom" should equal "filesystem:payments"
76
+ And the JSON output at "$.items[?(@.qualifiedName=='filesystem:log-correlation-id')].blocking.blocking" should equal "false"
77
+ And the JSON output at "$.items[?(@.qualifiedName=='filesystem:log-correlation-id')].inheritedFrom" should be null
78
+ And the JSON output at "$.items[*].view" should not contain "package-decisions"
79
+ And the JSON output at "$.effective.always" should contain "governance"
80
+ And the JSON output at "$.truncated" should be null
81
+
82
+ # ─────────────────────────────────────────────────────────────
83
+ # #290: naming a view or a path still holds the always-included views
84
+ # ─────────────────────────────────────────────────────────────
85
+
86
+ Scenario: Asking for one view still returns the always-included view first
87
+ When I run "kata tax context filesystem:payments.checkout.web-api --view package-decisions"
88
+ Then the command should succeed
89
+ And the output should contain "## Governing decisions and rules _(view: governance)_"
90
+ And the output should contain "## Decisions on the packages it uses _(view: package-decisions)_"
91
+ And the output should contain "⛔ blocking · **Encrypt card data**"
92
+ And the output should contain "`filesystem:pin-httpx`"
93
+ And the output should list "view: governance" before "view: package-decisions"
94
+
95
+ Scenario: Asking for one view in JSON returns the mandatory rule and the view's own item
96
+ When I run "kata tax context filesystem:payments.checkout.web-api --view package-decisions --format json"
97
+ Then the command should succeed
98
+ And the JSON output at "$.effective.views" should contain "governance"
99
+ And the JSON output at "$.effective.views" should contain "package-decisions"
100
+ And the JSON output at "$.effective.always" should contain "governance"
101
+ And the JSON output at "$.items[*].qualifiedName" should contain "filesystem:encrypt-card-data"
102
+ And the JSON output at "$.items[?(@.view=='package-decisions')]" should have 1 item
103
+ And the JSON output at "$.items[?(@.view=='package-decisions')].qualifiedName" should contain "filesystem:pin-httpx"
104
+
105
+ Scenario: Asking for a path still returns the always-included view
106
+ When I run "kata tax context filesystem:payments.checkout.web-api --path USES_PACKAGE:out --format json"
107
+ Then the command should succeed
108
+ And the JSON output at "$.items[*].qualifiedName" should contain "filesystem:encrypt-card-data"
109
+ And the JSON output at "$.items[*].qualifiedName" should contain "filesystem:httpx"
110
+ And the JSON output at "$.items[?(@.qualifiedName=='filesystem:httpx')].path" should equal "USES_PACKAGE:out"
111
+
112
+ # ─────────────────────────────────────────────────────────────
113
+ # Detail and depth
114
+ # ─────────────────────────────────────────────────────────────
115
+
116
+ Scenario: The detail level decides how much of an item is shown
117
+ When I run "kata tax context filesystem:payments.checkout.web-api --path USES_PACKAGE:out --detail refs --format json"
118
+ Then the command should succeed
119
+ And the JSON output at "$.items[?(@.qualifiedName=='filesystem:httpx')].detail" should equal "refs"
120
+ When I run "kata tax context filesystem:payments.checkout.web-api --path USES_PACKAGE:out --detail summary"
121
+ Then the command should succeed
122
+ And the output should contain "- **httpx** `filesystem:httpx` · version: 0.27"
123
+ And the output should contain "HTTP client"
124
+ When I run "kata tax context filesystem:payments.checkout.web-api --path USES_PACKAGE:out --detail full --format json"
125
+ Then the command should succeed
126
+ And the JSON output at "$.items[?(@.qualifiedName=='filesystem:httpx')].detail" should equal "full"
127
+ And the JSON output at "$.items[?(@.qualifiedName=='filesystem:httpx')].spec.version" should equal "0.27"
128
+
129
+ Scenario: The always-included view keeps its own detail when the request asks for more
130
+ When I run "kata tax context filesystem:payments.checkout.web-api --path USES_PACKAGE:out --detail full --format json"
131
+ Then the command should succeed
132
+ And the JSON output at "$.items[?(@.qualifiedName=='filesystem:encrypt-card-data')].detail" should equal "summary"
133
+
134
+ Scenario: The depth is how many times a single-step path is followed
135
+ When I run "kata tax context filesystem:payments.checkout.web-api --path CHILD_OF:out --no-inherit --depth 1 --detail refs --format json"
136
+ Then the command should succeed
137
+ And the JSON output at "$.items[?(@.path=='CHILD_OF:out')].qualifiedName" should contain "filesystem:payments.checkout"
138
+ And the JSON output at "$.items[?(@.path=='CHILD_OF:out')].qualifiedName" should not contain "filesystem:payments"
139
+ When I run "kata tax context filesystem:payments.checkout.web-api --path CHILD_OF:out --no-inherit --depth 2 --detail refs --format json"
140
+ Then the command should succeed
141
+ And the JSON output at "$.items[?(@.path=='CHILD_OF:out')].qualifiedName" should contain "filesystem:payments.checkout"
142
+ And the JSON output at "$.items[?(@.path=='CHILD_OF:out')].qualifiedName" should contain "filesystem:payments"
143
+ And the JSON output at "$.items[?(@.qualifiedName=='filesystem:payments' && @.path=='CHILD_OF:out')].depth" should equal "2"
144
+
145
+ # katalyst-taxonomy#303: a path that names CHILD_OF returns the ancestors with the default
146
+ # inheritance too; the map's `CHILD_OF:out → service ×1` row is no dead path.
147
+ Scenario: A path that names the parent relationship returns the ancestors with inheritance on
148
+ When I run "kata tax context filesystem:payments.checkout.web-api --path CHILD_OF:out --depth 2 --detail refs --format json"
149
+ Then the command should succeed
150
+ And the JSON output at "$.effective.inherit" should equal "true"
151
+ And the JSON output at "$.items[?(@.path=='CHILD_OF:out')].qualifiedName" should contain "filesystem:payments.checkout"
152
+ And the JSON output at "$.items[?(@.path=='CHILD_OF:out')].qualifiedName" should contain "filesystem:payments"
153
+ When I run "kata tax context filesystem:payments.checkout.web-api --path '*:out' --detail refs --format json"
154
+ Then the command should succeed
155
+ And the JSON output at "$.items[?(@.path=='*:out')].qualifiedName" should not contain "filesystem:payments.checkout"
156
+
157
+ Scenario: A depth above the built-in limit is lowered, and the answer says so
158
+ When I run "kata tax context filesystem:payments.checkout.web-api --path CHILD_OF:out --no-inherit --depth 9 --format json"
159
+ Then the command should succeed
160
+ And the JSON output at "$.effective.depth" should equal "4"
161
+ And the JSON output at "$.policy.clamped[?(@.control=='depth')].requested" should equal "9"
162
+ And the JSON output at "$.policy.clamped[?(@.control=='depth')].limitedBy" should equal "builtin"
163
+ When I run "kata tax context filesystem:payments.checkout.web-api --path CHILD_OF:out --no-inherit --depth 9"
164
+ Then the command should succeed
165
+ And the output should contain "- depth: asked 9, got 4 (builtin)"
166
+
167
+ Scenario: A detail level or step that is not allowed is refused before any reading
168
+ When I run "kata tax context filesystem:payments.checkout.web-api --detail huge"
169
+ Then the command should fail
170
+ And the exit code should be 2
171
+
172
+ # ─────────────────────────────────────────────────────────────
173
+ # Naming the node: FQTN ambiguity
174
+ # ─────────────────────────────────────────────────────────────
175
+
176
+ Scenario: An FQTN that two kinds share is refused, with the names that disambiguate it
177
+ When I run "kata tax create rule payments --set title=Payments --set severity=recommended -d 'A rule that is also called payments.'"
178
+ Then the command should succeed
179
+ When I run "kata tax context filesystem:payments"
180
+ Then the command should fail
181
+ And the output should contain "'filesystem:payments' names more than one node; use one of: filesystem:payments#area, filesystem:payments#rule"
182
+ When I run "kata tax context filesystem:payments#rule"
183
+ Then the command should succeed
184
+ And the output should contain "# Context: rule `filesystem:payments#rule`"
185
+ When I run "kata tax context filesystem:payments#area --map"
186
+ Then the command should succeed
187
+ And the output should contain "# Context: area `filesystem:payments#area`"
188
+
189
+ Scenario: A node that shares its name with another kind walks its own relationships
190
+ When I run "kata tax create rule payments --set title=Payments --set severity=recommended -d 'A rule that is also called payments.'"
191
+ Then the command should succeed
192
+ When I run "kata tax context 'filesystem:payments#area' --format json"
193
+ Then the command should succeed
194
+ And the JSON output at "$.node.qualifiedName" should equal "filesystem:payments#area"
195
+ And the JSON output at "$.items[*].qualifiedName" should contain "filesystem:encrypt-card-data"
196
+ And the JSON output at "$.items[*].qualifiedName" should contain "filesystem:use-postgres"
197
+ And the JSON output at "$.items[*].qualifiedName" should contain "filesystem:encrypt-card-data.scan-for-card-numbers"
198
+ And the JSON output at "$.dropped.unresolved" should equal "0"
199
+ When I run "kata tax context 'filesystem:payments#rule' --format json"
200
+ Then the command should succeed
201
+ And the JSON output at "$.items[*]" should have 0 items
202
+
203
+ Scenario: The children of a node that shares its name still find their parent
204
+ When I run "kata tax create rule payments --set title=Payments --set severity=recommended -d 'A rule that is also called payments.'"
205
+ Then the command should succeed
206
+ When I run "kata tax context filesystem:payments.checkout --path CHILD_OF:out --no-inherit --detail refs --format json"
207
+ Then the command should succeed
208
+ And the JSON output at "$.items[?(@.path=='CHILD_OF:out')].qualifiedName" should contain "filesystem:payments#area"
209
+
210
+ Scenario: A bare name of a nested node is refused with the name that works
211
+ When I run "kata tax context web-api"
212
+ Then the command should fail
213
+ And the output should contain "a node is named by its qualified name: use 'filesystem:payments.checkout.web-api'"
214
+
215
+ Scenario: An FQTN that one backend holds works without the backend
216
+ When I run "kata tax context payments.checkout.web-api --format json"
217
+ Then the command should succeed
218
+ And the JSON output at "$.node.qualifiedName" should equal "filesystem:payments.checkout.web-api"
219
+
220
+ # ─────────────────────────────────────────────────────────────
221
+ # #306: the markdown answer shows a node's fields
222
+ # ─────────────────────────────────────────────────────────────
223
+
224
+ Scenario: At full detail the markdown renders a list field as bullets
225
+ When I run "kata tax context filesystem:payments.checkout.web-api --path USES_PACKAGE:out --detail full"
226
+ Then the command should succeed
227
+ And the output should contain "- **httpx** `filesystem:httpx` · version: 0.27"
228
+ And the output should contain " - decisions:"
229
+ And the output should contain " - pin-httpx"
230
+ And the output should not contain "['pin-httpx']"
231
+
232
+ Scenario: At summary detail the markdown shows only the declared summary fields
233
+ When I run "kata tax context filesystem:payments.checkout.web-api --path USES_PACKAGE:out --detail summary"
234
+ Then the command should succeed
235
+ And the output should contain "- **httpx** `filesystem:httpx` · version: 0.27"
236
+ And the output should not contain "- decisions:"
237
+
238
+ Scenario: The markdown and the JSON hold the same items for the same budget
239
+ When I run "kata tax context filesystem:payments.checkout.web-api --path USES_PACKAGE:out --detail full --budget 120"
240
+ Then the command should succeed
241
+ When I record the item names of the context output
242
+ And I run "kata tax context filesystem:payments.checkout.web-api --path USES_PACKAGE:out --detail full --budget 120 --format json"
243
+ Then the command should succeed
244
+ And the context output should show the recorded item names
245
+
246
+ # ─────────────────────────────────────────────────────────────
247
+ # #307: one item reached by several routes
248
+ # ─────────────────────────────────────────────────────────────
249
+
250
+ Scenario: An item reached by two routes prints its shortest route and counts the other
251
+ When I run "kata tax relate filesystem:payments.checkout.web-api GOVERNED_BY filesystem:pin-httpx"
252
+ Then the command should succeed
253
+ When I run "kata tax context filesystem:payments.checkout.web-api --path USES_PACKAGE:out/GOVERNED_BY:out --detail summary"
254
+ Then the command should succeed
255
+ And the output should contain "why: ─GOVERNED_BY→ (+1 more route)"
256
+ And the output should not contain "package filesystem:httpx ─GOVERNED_BY→"
257
+
258
+ Scenario: At full detail the markdown lists every route
259
+ When I run "kata tax relate filesystem:payments.checkout.web-api GOVERNED_BY filesystem:pin-httpx"
260
+ Then the command should succeed
261
+ When I run "kata tax context filesystem:payments.checkout.web-api --path USES_PACKAGE:out/GOVERNED_BY:out --detail full"
262
+ Then the command should succeed
263
+ And the output should contain "why: ─GOVERNED_BY→"
264
+ And the output should contain "why: ─USES_PACKAGE→ package filesystem:httpx ─GOVERNED_BY→"
265
+ And the output should not contain "more route"
266
+
267
+ Scenario: The JSON keeps every route, the shortest first
268
+ When I run "kata tax relate filesystem:payments.checkout.web-api GOVERNED_BY filesystem:pin-httpx"
269
+ Then the command should succeed
270
+ When I run "kata tax context filesystem:payments.checkout.web-api --path USES_PACKAGE:out/GOVERNED_BY:out --detail summary --format json"
271
+ Then the command should succeed
272
+ And the JSON output at "$.items[?(@.qualifiedName=='filesystem:pin-httpx')].why[*]" should have 2 items
273
+ And the JSON output at "$.items[?(@.qualifiedName=='filesystem:pin-httpx')].why[0].path" should equal "GOVERNED_BY:out"
274
+
@@ -0,0 +1,265 @@
1
+ @cli @area-context
2
+ Feature: Kata CLI - Context: the budget, profile policy, and lint of the policy
3
+ As an operator deciding what each agent role may pull into its context
4
+ I want a size budget that cuts the least important items first, and policy per profile that only narrows
5
+ So that a small answer still holds the rule that blocks, and a profile never widens what the schema allows
6
+
7
+ # docs/plugins/context/index.md ("The budget", "Policy", "What kata tax lint checks"), against the
8
+ # same payments catalogue as 52_kata_context.feature.
9
+ #
10
+ # katalyst-taxonomy#291: the budget measures the content of the items, not the serialisation, so
11
+ # JSON and markdown return the same items for the same budget.
12
+ # katalyst-taxonomy#292: lint warns when a profile's policy makes an always-included view
13
+ # unreachable.
14
+ #
15
+ # The plugin's config.yaml section holds policy for every profile and for one profile. A
16
+ # profile is a role: `reviewer` reads decisions and rules in summary and follows no incoming
17
+ # step and no package; `ci-bot` reads names only, in a small answer.
18
+
19
+ Background:
20
+ Given the payments catalogue is installed
21
+
22
+ # ─────────────────────────────────────────────────────────────
23
+ # The budget
24
+ # ─────────────────────────────────────────────────────────────
25
+
26
+ @smoke
27
+ Scenario: A small budget cuts the items that do not block first, and says what it cut
28
+ When I run "kata tax context filesystem:payments.checkout.web-api --budget 100 --format json"
29
+ Then the command should succeed
30
+ And the JSON output at "$.items[*].qualifiedName" should contain "filesystem:encrypt-card-data"
31
+ And the JSON output at "$.items[?(@.qualifiedName=='filesystem:encrypt-card-data')].blocking.blocking" should equal "true"
32
+ And the JSON output at "$.truncated.cutBlocking" should equal "0"
33
+ And the JSON output at "$.truncated.cut[*].qualifiedNames[*]" should contain "filesystem:log-correlation-id"
34
+ And the JSON output at "$.truncated.cut[*].qualifiedNames[*]" should contain "filesystem:encrypt-card-data.scan-for-card-numbers"
35
+ And the JSON output at "$.truncated.cut[*].qualifiedNames[*]" should not contain "filesystem:encrypt-card-data"
36
+ And the JSON output at "$.truncated.hint.budget" should equal "270"
37
+
38
+ Scenario: The markdown names what the budget cut, and how to get it
39
+ When I run "kata tax context filesystem:payments.checkout.web-api --budget 100"
40
+ Then the command should succeed
41
+ And the output should contain "⛔ blocking · **Encrypt card data** `filesystem:encrypt-card-data`"
42
+ And the output should contain "## Cut by the budget (100 tokens;"
43
+ And the output should contain "view governance, `FOLLOWS_RULE:out`: 1 cut — `filesystem:log-correlation-id`"
44
+ And the output should contain "More: ask again with budget 270."
45
+
46
+ Scenario: The budget returns the same items over JSON and over markdown
47
+ When I run "kata tax context filesystem:payments.checkout.web-api --budget 100 --format json"
48
+ And I record the item names of the context output
49
+ And I run "kata tax context filesystem:payments.checkout.web-api --budget 100 --format markdown"
50
+ Then the command should succeed
51
+ And the context output should show the recorded item names
52
+
53
+ Scenario: Asking again with the hinted budget returns everything
54
+ When I run "kata tax context filesystem:payments.checkout.web-api --budget 270 --format json"
55
+ Then the command should succeed
56
+ And the JSON output at "$.items[*]" should have 5 items
57
+ And the JSON output at "$.truncated" should be null
58
+
59
+ # ─────────────────────────────────────────────────────────────
60
+ # A profile narrows a view
61
+ # ─────────────────────────────────────────────────────────────
62
+
63
+ @smoke
64
+ Scenario: A profile's policy narrows the answer, and the notes say what it hid
65
+ Given I run "kata config profile create reviewer --copy-from local"
66
+ And the context plugin is configured with:
67
+ """
68
+ policy:
69
+ budget: { max: 8000 }
70
+ profiles:
71
+ reviewer:
72
+ policy:
73
+ detail: [refs, summary]
74
+ paths: { deny: ["*:in", "USES_PACKAGE"] }
75
+ """
76
+ When I run "kata --profile reviewer tax context filesystem:payments.checkout.web-api --view package-decisions --detail full --format json"
77
+ Then the command should succeed
78
+ And the JSON output at "$.items[*].qualifiedName" should contain "filesystem:encrypt-card-data"
79
+ And the JSON output at "$.items[*].qualifiedName" should not contain "filesystem:encrypt-card-data.scan-for-card-numbers"
80
+ And the JSON output at "$.items[*].qualifiedName" should not contain "filesystem:pin-httpx"
81
+ And the JSON output at "$.policy.layers" should contain "profile:reviewer"
82
+ And the JSON output at "$.policy.notes[*].text" should contain "view 'governance' path 'FOLLOWS_RULE:out/CHILD_OF:in:check' is hidden by profile:reviewer"
83
+ And the JSON output at "$.policy.notes[*].text" should contain "view 'package-decisions' path 'USES_PACKAGE:out/GOVERNED_BY:out' is hidden by profile:reviewer"
84
+ And the JSON output at "$.policy.clamped[?(@.control=='detail')].requested" should equal "full"
85
+ And the JSON output at "$.policy.clamped[?(@.control=='detail')].effective" should equal "summary"
86
+ And the JSON output at "$.policy.clamped[?(@.control=='detail')].limitedBy" should equal "profile:reviewer"
87
+
88
+ Scenario: The same request under the default profile is not narrowed
89
+ Given I run "kata config profile create reviewer --copy-from local"
90
+ And the context plugin is configured with:
91
+ """
92
+ profiles:
93
+ reviewer:
94
+ policy:
95
+ paths: { deny: ["*:in", "USES_PACKAGE"] }
96
+ """
97
+ When I run "kata tax context filesystem:payments.checkout.web-api --view package-decisions --detail full --format json"
98
+ Then the command should succeed
99
+ And the JSON output at "$.items[*].qualifiedName" should contain "filesystem:encrypt-card-data.scan-for-card-numbers"
100
+ And the JSON output at "$.items[*].qualifiedName" should contain "filesystem:pin-httpx"
101
+ And the JSON output at "$.policy.layers" should not contain "profile:reviewer"
102
+ And the JSON output at "$.policy.notes[*]" should have 0 items
103
+
104
+ Scenario: The markdown ends with the policy layers and the notes
105
+ Given I run "kata config profile create reviewer --copy-from local"
106
+ And the context plugin is configured with:
107
+ """
108
+ profiles:
109
+ reviewer:
110
+ policy:
111
+ detail: [refs, summary]
112
+ paths: { deny: ["*:in", "USES_PACKAGE"] }
113
+ """
114
+ When I run "kata --profile reviewer tax context filesystem:payments.checkout.web-api --view package-decisions --detail full"
115
+ Then the command should succeed
116
+ And the output should contain "_Policy: builtin → kind:component → profile:reviewer_"
117
+ And the output should contain "- view 'governance' path 'FOLLOWS_RULE:out/CHILD_OF:in:check' is hidden by profile:reviewer"
118
+ And the output should contain "- detail: asked full, got summary (profile:reviewer)"
119
+ And the output should not contain "Scan for card numbers"
120
+
121
+ Scenario: A profile cannot widen a limit: a request above its budget is lowered
122
+ Given I run "kata config profile create ci-bot --copy-from local"
123
+ And the context plugin is configured with:
124
+ """
125
+ profiles:
126
+ ci-bot:
127
+ policy:
128
+ detail: [refs]
129
+ budget: { max: 100 }
130
+ defaults: { budget: 100 }
131
+ """
132
+ When I run "kata --profile ci-bot tax context filesystem:payments.checkout.web-api --budget 1000 --format json"
133
+ Then the command should succeed
134
+ And the JSON output at "$.effective.budget" should equal "100"
135
+ And the JSON output at "$.policy.clamped[?(@.control=='budget')].requested" should equal "1000"
136
+ And the JSON output at "$.policy.clamped[?(@.control=='budget')].effective" should equal "100"
137
+ And the JSON output at "$.policy.clamped[?(@.control=='budget')].limitedBy" should equal "profile:ci-bot"
138
+ And the JSON output at "$.truncated.hint.budget" should equal "100"
139
+ And the JSON output at "$.items[?(@.qualifiedName=='filesystem:encrypt-card-data')].blocking.blocking" should equal "true"
140
+
141
+ Scenario: A profile can take a view out of the always-included views, and the answer says so
142
+ Given I run "kata config profile create ci-bot --copy-from local"
143
+ And the context plugin is configured with:
144
+ """
145
+ profiles:
146
+ ci-bot:
147
+ policy:
148
+ always_include: []
149
+ """
150
+ When I run "kata --profile ci-bot tax context filesystem:payments.checkout.web-api --path USES_PACKAGE:out --format json"
151
+ Then the command should succeed
152
+ And the JSON output at "$.items[*].qualifiedName" should contain "filesystem:httpx"
153
+ And the JSON output at "$.items[*].qualifiedName" should not contain "filesystem:encrypt-card-data"
154
+ And the JSON output at "$.dropped.always[0].view" should equal "governance"
155
+ And the JSON output at "$.dropped.always[0].limitedBy" should equal "profile:ci-bot"
156
+ When I run "kata --profile ci-bot tax context filesystem:payments.checkout.web-api --map --format json"
157
+ Then the command should succeed
158
+ And the JSON output at "$.views[?(@.name=='governance')].always" should equal "false"
159
+ And the JSON output at "$.views[?(@.name=='governance')].droppedBy" should equal "profile:ci-bot"
160
+
161
+ Scenario: The effective policy of a node can be printed
162
+ Given I run "kata config profile create reviewer --copy-from local"
163
+ And the context plugin is configured with:
164
+ """
165
+ profiles:
166
+ reviewer:
167
+ policy:
168
+ detail: [refs, summary]
169
+ """
170
+ When I run "kata --profile reviewer tax context filesystem:payments.checkout.web-api --policy"
171
+ Then the command should succeed
172
+ And the output should be valid JSON
173
+ And the JSON output at "$.effective.layers" should contain "kind:component"
174
+ And the JSON output at "$.effective.layers" should contain "profile:reviewer"
175
+ And the JSON output at "$.effective.detail" should not contain "full"
176
+
177
+ # ─────────────────────────────────────────────────────────────
178
+ # #292: kata tax lint warns when a profile hides an always-included view
179
+ # ─────────────────────────────────────────────────────────────
180
+
181
+ Scenario: Lint warns when a profile hides a path of an always-included view
182
+ Given I run "kata config profile create reviewer --copy-from local"
183
+ And the context plugin is configured with:
184
+ """
185
+ profiles:
186
+ reviewer:
187
+ policy:
188
+ paths: { deny: ["*:in", "USES_PACKAGE"] }
189
+ """
190
+ When I run "kata tax lint"
191
+ Then the command should succeed
192
+ And the output should contain "Rule warnings"
193
+ And the output should contain "[context-policy] kind 'component': profile:reviewer hides always-included view 'governance' path 'FOLLOWS_RULE:out/CHILD_OF:in:check'"
194
+ And the output should contain "[context-policy] kind 'service': profile:reviewer hides always-included view 'governance' path 'FOLLOWS_RULE:out/CHILD_OF:in:check'"
195
+ And the output should contain "[context-policy] kind 'area': profile:reviewer hides always-included view 'governance' path 'FOLLOWS_RULE:out/CHILD_OF:in:check'"
196
+
197
+ Scenario: Lint says when the hidden path can reach a blocking item
198
+ Given I run "kata config profile create triage --copy-from local"
199
+ And the context plugin is configured with:
200
+ """
201
+ profiles:
202
+ triage:
203
+ policy:
204
+ paths: { deny: ["FOLLOWS_RULE"] }
205
+ inheritance: false
206
+ """
207
+ When I run "kata tax lint"
208
+ Then the command should succeed
209
+ And the output should contain "[context-policy] kind 'component': profile:triage turns inheritance off, so always-included view 'governance' loses what its ancestors carry, which can include blocking items (rule)"
210
+ And the output should contain "[context-policy] kind 'component': profile:triage hides always-included view 'governance' path 'FOLLOWS_RULE:out'; it can reach blocking items (rule)"
211
+
212
+ Scenario: Lint warns when a profile takes the view out of the always-included views
213
+ Given I run "kata config profile create ci-bot --copy-from local"
214
+ And the context plugin is configured with:
215
+ """
216
+ profiles:
217
+ ci-bot:
218
+ policy:
219
+ always_include: []
220
+ """
221
+ When I run "kata tax lint"
222
+ Then the command should succeed
223
+ And the output should contain "[context-policy] kind 'component': profile:ci-bot"
224
+ And the output should contain "always-included view 'governance'"
225
+
226
+ Scenario: Lint warns when a max_depth is shorter than a path of the always-included view
227
+ Given I run "kata config profile create shallow --copy-from local"
228
+ And the context plugin is configured with:
229
+ """
230
+ profiles:
231
+ shallow:
232
+ policy:
233
+ max_depth: 1
234
+ """
235
+ When I run "kata tax lint"
236
+ Then the command should succeed
237
+ And the output should contain "[context-policy] kind 'component': profile:shallow sets max_depth 1, shorter than always-included view 'governance' path 'FOLLOWS_RULE:out/CHILD_OF:in:check'"
238
+
239
+ Scenario: Lint is silent when no profile narrows the always-included view
240
+ Given I run "kata config profile create reviewer --copy-from local"
241
+ And the context plugin is configured with:
242
+ """
243
+ profiles:
244
+ reviewer:
245
+ policy:
246
+ detail: [refs, summary]
247
+ budget: { max: 2000 }
248
+ """
249
+ When I run "kata tax lint"
250
+ Then the command should succeed
251
+ And the output should not contain "context-policy"
252
+
253
+ Scenario: A policy that breaks the plugin's schema fails closed
254
+ Given I run "kata config profile create reviewer --copy-from local"
255
+ And the context plugin is configured with:
256
+ """
257
+ profiles:
258
+ reviewer:
259
+ policy:
260
+ budget: { max: lots }
261
+ """
262
+ When I run "kata --profile reviewer tax context filesystem:payments.checkout.web-api"
263
+ Then the command should fail
264
+ And the output should contain "budget.max"
265
+ And the output should contain "is not of type 'integer'"
@@ -0,0 +1,190 @@
1
+ @mcp @area-context
2
+ Feature: MCP Server - Context: taxonomy_context_map and taxonomy_context
3
+ As an AI agent about to change a node
4
+ I want the map of what surrounds it and one call that returns the decisions and rules that apply
5
+ So that I read a rule the node inherits, and a blocking item, before I act
6
+
7
+ # docs/plugins/context/index.md ("The surfaces"), against the service catalogue of
8
+ # examples/catalogue with the payments model of plugins/context/xspec/fixtures/context-catalogue. Over MCP
9
+ # the format defaults to json and the answer is an envelope: {ok, data}. With
10
+ # format=markdown, data is {markdown, result}. The same service answers the CLI, so the
11
+ # details are in 52_kata_context.feature and 53_kata_context_budget_policy.feature; these
12
+ # scenarios hold the essentials of the tools.
13
+
14
+ Background:
15
+ Given the MCP server is connected
16
+ And the payments catalogue is installed in the MCP workspace
17
+
18
+ Scenario: The server exposes the context tools
19
+ Then the MCP server should expose a tool named "taxonomy_context_map"
20
+ And the MCP server should expose a tool named "taxonomy_context"
21
+
22
+ @smoke
23
+ Scenario: The map lists the ancestors, the views and what can be asked next
24
+ When I call MCP tool "taxonomy_context_map" with:
25
+ | key | value |
26
+ | node | filesystem:payments.checkout.web-api |
27
+ Then the result should succeed
28
+ And the JSON result at "$.ok" should equal "true"
29
+ And the JSON result at "$.data.node.qualifiedName" should equal "filesystem:payments.checkout.web-api"
30
+ And the JSON result at "$.data.ancestors[*].qualifiedName" should contain "filesystem:payments.checkout"
31
+ And the JSON result at "$.data.ancestors[*].qualifiedName" should contain "filesystem:payments"
32
+ And the JSON result at "$.data.views[?(@.name=='governance')].always" should equal "true"
33
+ And the JSON result at "$.data.views[?(@.name=='package-decisions')].always" should equal "false"
34
+ And the JSON result at "$.data.relationships[*].path" should contain "USES_PACKAGE:out"
35
+ And the JSON result at "$.data.options.steps" should contain "GOVERNED_BY:out"
36
+ And the JSON result at "$.data.always.items[*].qualifiedName" should contain "filesystem:encrypt-card-data"
37
+
38
+ @smoke
39
+ Scenario: A call with no path and no view returns the always-included view, the mandatory rule first
40
+ When I call MCP tool "taxonomy_context" with:
41
+ | key | value |
42
+ | node | filesystem:payments.checkout.web-api |
43
+ Then the result should succeed
44
+ And the JSON result at "$.data.items[0].qualifiedName" should equal "filesystem:encrypt-card-data"
45
+ And the JSON result at "$.data.items[0].blocking.blocking" should equal "true"
46
+ And the JSON result at "$.data.items[0].inheritedFrom" should equal "filesystem:payments"
47
+ And the JSON result at "$.data.items[?(@.qualifiedName=='filesystem:log-correlation-id')].blocking.blocking" should equal "false"
48
+ And the JSON result at "$.data.effective.always" should contain "governance"
49
+ And the JSON result at "$.data.truncated" should be null
50
+
51
+ Scenario: A named view still holds the always-included view
52
+ When I call MCP tool "taxonomy_context" with:
53
+ | key | value |
54
+ | node | filesystem:payments.checkout.web-api |
55
+ | views | ["package-decisions"] |
56
+ Then the result should succeed
57
+ And the JSON result at "$.data.items[*].qualifiedName" should contain "filesystem:encrypt-card-data"
58
+ And the JSON result at "$.data.items[?(@.view=='package-decisions')].qualifiedName" should contain "filesystem:pin-httpx"
59
+
60
+ Scenario: A path still holds the always-included view
61
+ When I call MCP tool "taxonomy_context" with:
62
+ | key | value |
63
+ | node | filesystem:payments.checkout.web-api |
64
+ | paths | ["USES_PACKAGE:out"] |
65
+ Then the result should succeed
66
+ And the JSON result at "$.data.items[*].qualifiedName" should contain "filesystem:encrypt-card-data"
67
+ And the JSON result at "$.data.items[*].qualifiedName" should contain "filesystem:httpx"
68
+
69
+ Scenario: The detail and the depth decide how much is returned
70
+ When I call MCP tool "taxonomy_context" with:
71
+ | key | value |
72
+ | node | filesystem:payments.checkout.web-api |
73
+ | paths | ["USES_PACKAGE:out"] |
74
+ | detail | full |
75
+ Then the result should succeed
76
+ And the JSON result at "$.data.items[?(@.qualifiedName=='filesystem:httpx')].detail" should equal "full"
77
+ And the JSON result at "$.data.items[?(@.qualifiedName=='filesystem:httpx')].spec.version" should equal "0.27"
78
+ When I call MCP tool "taxonomy_context" with:
79
+ | key | value |
80
+ | node | filesystem:payments.checkout.web-api |
81
+ | paths | ["CHILD_OF:out"] |
82
+ | depth | 2 |
83
+ | inherit | false |
84
+ | detail | refs |
85
+ Then the result should succeed
86
+ And the JSON result at "$.data.items[?(@.path=='CHILD_OF:out')].qualifiedName" should contain "filesystem:payments"
87
+
88
+ Scenario: A small budget cuts the items that do not block first, over the default JSON format
89
+ When I call MCP tool "taxonomy_context" with:
90
+ | key | value |
91
+ | node | filesystem:payments.checkout.web-api |
92
+ | budget | 100 |
93
+ Then the result should succeed
94
+ And the JSON result at "$.data.items[*].qualifiedName" should contain "filesystem:encrypt-card-data"
95
+ And the JSON result at "$.data.items[?(@.qualifiedName=='filesystem:encrypt-card-data')].blocking.blocking" should equal "true"
96
+ And the JSON result at "$.data.truncated.cutBlocking" should equal "0"
97
+ And the JSON result at "$.data.truncated.cut[*].qualifiedNames[*]" should contain "filesystem:log-correlation-id"
98
+
99
+ Scenario: The budget returns the same items in JSON and in markdown
100
+ When I call MCP tool "taxonomy_context" with:
101
+ | key | value |
102
+ | node | filesystem:payments.checkout.web-api |
103
+ | budget | 100 |
104
+ And I record the item names of the MCP context result
105
+ And I call MCP tool "taxonomy_context" with:
106
+ | key | value |
107
+ | node | filesystem:payments.checkout.web-api |
108
+ | budget | 100 |
109
+ | format | markdown |
110
+ Then the result should succeed
111
+ And the MCP context result should show the recorded item names
112
+ And the markdown of the MCP context result should show the recorded item names
113
+ And the JSON result at "$.data.markdown" should contain "Cut by the budget (100 tokens;"
114
+
115
+ Scenario: A profile's policy narrows the answer, and the notes say what it hid
116
+ Given the MCP workspace has a profile "reviewer" that is active
117
+ And the context plugin is configured in the MCP workspace with:
118
+ """
119
+ profiles:
120
+ reviewer:
121
+ policy:
122
+ detail: [refs, summary]
123
+ paths: { deny: ["*:in", "USES_PACKAGE"] }
124
+ """
125
+ When I call MCP tool "taxonomy_context" with:
126
+ | key | value |
127
+ | node | filesystem:payments.checkout.web-api |
128
+ | views | ["package-decisions"] |
129
+ | detail | full |
130
+ Then the result should succeed
131
+ And the JSON result at "$.data.items[*].qualifiedName" should contain "filesystem:encrypt-card-data"
132
+ And the JSON result at "$.data.items[*].qualifiedName" should not include the value "filesystem:pin-httpx"
133
+ And the JSON result at "$.data.items[*].qualifiedName" should not include the value "filesystem:encrypt-card-data.scan-for-card-numbers"
134
+ And the JSON result at "$.data.policy.layers" should contain "profile:reviewer"
135
+ And the JSON result at "$.data.policy.notes[*].text" should contain "view 'package-decisions' path 'USES_PACKAGE:out/GOVERNED_BY:out' is hidden by profile:reviewer"
136
+ And the JSON result at "$.data.policy.clamped[?(@.control=='detail')].effective" should equal "summary"
137
+
138
+ Scenario: An FQTN that two kinds share is refused, with the names that disambiguate it
139
+ Given a config file ".global/taxonomy/catalogue/rules/payments.yaml" in the MCP workspace with:
140
+ """
141
+ apiVersion: katalyst.taxonomy/v2alpha
142
+ kind: rule
143
+ metadata:
144
+ name: payments
145
+ spec:
146
+ title: Payments
147
+ severity: recommended
148
+ description: A rule that is also called payments.
149
+ owners: [team@example.com]
150
+ """
151
+ When I call MCP tool "taxonomy_context" with:
152
+ | key | value |
153
+ | node | filesystem:payments |
154
+ Then the JSON result at "$.ok" should equal "false"
155
+ And the JSON result at "$.error.code" should equal "INVALID_INPUT"
156
+ And the result should contain "filesystem:payments#area"
157
+ And the result should contain "filesystem:payments#rule"
158
+ When I call MCP tool "taxonomy_context_map" with:
159
+ | key | value |
160
+ | node | filesystem:payments#rule |
161
+ Then the result should succeed
162
+ And the JSON result at "$.data.node.qualifiedName" should equal "filesystem:payments#rule"
163
+
164
+ Scenario: A node that shares its name with another kind walks its own relationships
165
+ Given a config file ".global/taxonomy/catalogue/rules/payments.yaml" in the MCP workspace with:
166
+ """
167
+ apiVersion: katalyst.taxonomy/v2alpha
168
+ kind: rule
169
+ metadata:
170
+ name: payments
171
+ spec:
172
+ title: Payments
173
+ severity: recommended
174
+ description: A rule that is also called payments.
175
+ owners: [team@example.com]
176
+ """
177
+ When I call MCP tool "taxonomy_context" with:
178
+ | key | value |
179
+ | node | filesystem:payments#area |
180
+ Then the result should succeed
181
+ And the JSON result at "$.data.items[*].qualifiedName" should contain "filesystem:use-postgres"
182
+ And the JSON result at "$.data.dropped.unresolved" should equal "0"
183
+
184
+ Scenario: A bare name of a nested node is refused with the name that works
185
+ When I call MCP tool "taxonomy_context" with:
186
+ | key | value |
187
+ | node | web-api |
188
+ Then the JSON result at "$.ok" should equal "false"
189
+ And the JSON result at "$.error.code" should equal "NOT_FOUND"
190
+ And the JSON result at "$.error.message" should contain "filesystem:payments.checkout.web-api"
@@ -0,0 +1,13 @@
1
+ apiVersion: katalyst.taxonomy/v2alpha
2
+ kind: area
3
+ metadata:
4
+ name: payments
5
+ spec:
6
+ owner: payments-team
7
+ description: Everything that moves money
8
+ owners:
9
+ - team@example.com
10
+ decisions:
11
+ - use-postgres
12
+ rules:
13
+ - encrypt-card-data
@@ -0,0 +1,12 @@
1
+ apiVersion: katalyst.taxonomy/v2alpha
2
+ kind: check
3
+ metadata:
4
+ name: "scan-for-card-numbers"
5
+ spec:
6
+ title: "Scan for card numbers"
7
+ command: "make scan-pan"
8
+ description: "Scans the code and the log samples for card numbers."
9
+ owners:
10
+ - "team@example.com"
11
+ parents:
12
+ node: "encrypt-card-data"
@@ -0,0 +1,15 @@
1
+ apiVersion: katalyst.taxonomy/v2alpha
2
+ kind: component
3
+ metadata:
4
+ name: web-api
5
+ spec:
6
+ language: python
7
+ description: The HTTP API that the checkout flow exposes to the storefront.
8
+ owners:
9
+ - team@example.com
10
+ parents:
11
+ node: payments.checkout
12
+ rules:
13
+ - log-correlation-id
14
+ packages:
15
+ - httpx
@@ -0,0 +1,10 @@
1
+ apiVersion: katalyst.taxonomy/v2alpha
2
+ kind: decision
3
+ metadata:
4
+ name: "async-events"
5
+ spec:
6
+ title: "Publish events, do not call"
7
+ status: "accepted"
8
+ description: "The checkout service tells other services about an order by publishing an event."
9
+ owners:
10
+ - "team@example.com"
@@ -0,0 +1,10 @@
1
+ apiVersion: katalyst.taxonomy/v2alpha
2
+ kind: decision
3
+ metadata:
4
+ name: "pin-httpx"
5
+ spec:
6
+ title: "Pin httpx to 0.27"
7
+ status: "accepted"
8
+ description: "The web-api component pins httpx to the 0.27 series."
9
+ owners:
10
+ - "team@example.com"
@@ -0,0 +1,10 @@
1
+ apiVersion: katalyst.taxonomy/v2alpha
2
+ kind: decision
3
+ metadata:
4
+ name: "use-postgres"
5
+ spec:
6
+ title: "Use PostgreSQL"
7
+ status: "accepted"
8
+ description: "Every service in the payments area stores its data in PostgreSQL."
9
+ owners:
10
+ - "team@example.com"
@@ -0,0 +1,11 @@
1
+ apiVersion: katalyst.taxonomy/v2alpha
2
+ kind: package
3
+ metadata:
4
+ name: httpx
5
+ spec:
6
+ version: '0.27'
7
+ description: HTTP client
8
+ owners:
9
+ - team@example.com
10
+ decisions:
11
+ - pin-httpx
@@ -0,0 +1,10 @@
1
+ apiVersion: katalyst.taxonomy/v2alpha
2
+ kind: rule
3
+ metadata:
4
+ name: "encrypt-card-data"
5
+ spec:
6
+ title: "Encrypt card data"
7
+ severity: "mandatory"
8
+ description: "Card numbers are encrypted at rest and in transit. No log line contains a card number."
9
+ owners:
10
+ - "team@example.com"
@@ -0,0 +1,10 @@
1
+ apiVersion: katalyst.taxonomy/v2alpha
2
+ kind: rule
3
+ metadata:
4
+ name: "log-correlation-id"
5
+ spec:
6
+ title: "Log a correlation id"
7
+ severity: "recommended"
8
+ description: "Every log line of a request carries the correlation id of the request."
9
+ owners:
10
+ - "team@example.com"
@@ -0,0 +1,13 @@
1
+ apiVersion: katalyst.taxonomy/v2alpha
2
+ kind: service
3
+ metadata:
4
+ name: checkout
5
+ spec:
6
+ tier: 1
7
+ description: The checkout flow
8
+ owners:
9
+ - team@example.com
10
+ parents:
11
+ node: payments
12
+ decisions:
13
+ - async-events
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "katalyst-plugin-context-xspec",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "description": "The context plugin's xspec suite. Its dependencies and the shared step library are xspec/'s (npm workspace at the repository root).",
7
+ "scripts": {
8
+ "gen": "bddgen",
9
+ "test": "bddgen && playwright test",
10
+ "typecheck": "tsc --noEmit",
11
+ "smoke": "../../../xspec/scripts/run-tags.sh '@smoke'",
12
+ "tags": "../../../xspec/scripts/run-tags.sh",
13
+ "clean": "rm -rf .features-gen test-results cucumber-report blob-report playwright-report"
14
+ }
15
+ }
@@ -0,0 +1,5 @@
1
+ // The context plugin's suite: its features (features/<surface>/), fixtures and steps (steps/).
2
+ // The shared step library (xspec/support/) and config (xspec/playwright.base.ts) are the root's.
3
+ import { xspecConfig } from '../../../xspec/playwright.base.js';
4
+
5
+ export default xspecConfig(import.meta.url);
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Step definitions for the context plugin features (kata tax context, taxonomy_context_map,
3
+ * taxonomy_context).
4
+ *
5
+ * The fixture is the neutral service catalogue of `examples/catalogue`, installed the way
6
+ * docs/user-guide/getting-started.md installs it (`kata tax bundle add`), with the payments
7
+ * nodes of `plugins/context/xspec/fixtures/context-catalogue` copied in beside it. The nodes are written
8
+ * as files rather than through ~20 `kata tax create` calls, which would be most of every
9
+ * scenario's time.
10
+ */
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { expect } from '@playwright/test';
15
+ import { createBdd } from 'playwright-bdd';
16
+ import { buildEnv, createTempDir, dedent, runCommand } from '../../../../xspec/support/cli/steps.js';
17
+ import { runKata } from '../../../../xspec/support/mcp/steps.js';
18
+ import { repoCommand } from '../../../../xspec/support/runner.js';
19
+ import type { CliWorld, test as sharedTest } from '../../../../xspec/support/fixtures.js';
20
+ import type { McpPort, McpToolResult } from '../../../../xspec/support/mcp/port.js';
21
+
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const repoRoot = path.resolve(__dirname, '../../../..');
24
+ const catalogueExample = path.join(repoRoot, 'examples', 'catalogue');
25
+ const paymentsFixture = path.join(__dirname, '..', 'fixtures', 'context-catalogue', 'catalogue');
26
+
27
+ /** Copy the payments nodes into the repository, beside what the bundle's kinds store. */
28
+ function copyPayments(repoDir: string): void {
29
+ const target = path.join(repoDir, '.global', 'taxonomy', 'catalogue');
30
+ fs.mkdirSync(target, { recursive: true });
31
+ fs.cpSync(paymentsFixture, target, { recursive: true });
32
+ }
33
+
34
+ /**
35
+ * The text that goes under `spec:` of config.yaml to configure the context plugin: the
36
+ * docstring is the plugin's `config` mapping.
37
+ */
38
+ function pluginsBlock(configYaml: string): string {
39
+ const body = dedent(configYaml)
40
+ .split('\n')
41
+ .map((line) => (line.length > 0 ? ` ${line}` : line))
42
+ .join('\n');
43
+ return [' plugins:', ' - name: context', ' config:', body, ''].join('\n');
44
+ }
45
+
46
+ /** Append the plugin block to a config file whose `spec:` is its last mapping. */
47
+ function appendPluginConfig(configFile: string, configYaml: string): void {
48
+ const current = fs.readFileSync(configFile, 'utf-8');
49
+ expect(current, `${configFile} already configures plugins`).not.toContain('\n plugins:');
50
+ fs.writeFileSync(configFile, `${current.replace(/\n*$/, '\n')}${pluginsBlock(configYaml)}`, 'utf-8');
51
+ }
52
+
53
+ /**
54
+ * The qualified names of the items in a context answer: from the JSON result (a CLI
55
+ * `--format json` answer, or an MCP envelope with or without markdown beside it), or from
56
+ * the markdown, whose item bullets come before the cut report and the policy line.
57
+ */
58
+ export function contextItemNames(text: string): string[] {
59
+ let parsed: unknown;
60
+ try {
61
+ parsed = JSON.parse(text);
62
+ } catch {
63
+ parsed = undefined;
64
+ }
65
+ if (parsed !== undefined && parsed !== null && typeof parsed === 'object') {
66
+ const root = parsed as Record<string, unknown>;
67
+ const data = (root.data ?? root) as Record<string, unknown>;
68
+ const result = (data.result ?? data) as { items?: Array<{ qualifiedName: string }> };
69
+ expect(Array.isArray(result.items), `no items in ${text.substring(0, 300)}`).toBe(true);
70
+ return result.items!.map((item) => item.qualifiedName);
71
+ }
72
+ const names: string[] = [];
73
+ for (const line of text.split('\n')) {
74
+ if (line.startsWith('## Cut by the budget') || line.startsWith('_Policy:')) break;
75
+ const match = /^- .*?`([^`]+)`/.exec(line);
76
+ if (match) names.push(match[1]);
77
+ }
78
+ return names;
79
+ }
80
+
81
+ const recorded = new Map<string, string[]>();
82
+
83
+ export function registerContextSteps(test: typeof sharedTest): void {
84
+ const { Given, When, Then } = createBdd(test);
85
+
86
+ // ── CLI ────────────────────────────────────────────────────────────────
87
+
88
+ Given(
89
+ 'the payments catalogue is installed',
90
+ { tags: '@cli' },
91
+ async ({ cliWorld }: { cliWorld: CliWorld }) => {
92
+ cliWorld.workDir = createTempDir();
93
+ runCommand(cliWorld, repoCommand(repoRoot, 'kata tax init -y'));
94
+ expect(cliWorld.success, `init failed: ${cliWorld.stdout}${cliWorld.stderr}`).toBe(true);
95
+ runCommand(cliWorld, repoCommand(repoRoot, `kata tax bundle add -y "${catalogueExample}"`));
96
+ expect(cliWorld.success, `bundle add failed: ${cliWorld.stdout}${cliWorld.stderr}`).toBe(true);
97
+ copyPayments(cliWorld.workDir);
98
+ },
99
+ );
100
+
101
+ Given(
102
+ 'the context plugin is configured with:',
103
+ { tags: '@cli' },
104
+ async ({ cliWorld }: { cliWorld: CliWorld }, docString: string) => {
105
+ const home = buildEnv(cliWorld).KATALYST_HOME as string;
106
+ appendPluginConfig(path.join(home, 'config', 'config.yaml'), docString);
107
+ },
108
+ );
109
+
110
+ When(
111
+ 'I record the item names of the context output',
112
+ { tags: '@cli' },
113
+ async ({ cliWorld }: { cliWorld: CliWorld }) => {
114
+ expect(cliWorld.exitCode, cliWorld.stderr).toBe(0);
115
+ const names = contextItemNames(cliWorld.stdout);
116
+ expect(names.length, 'the answer has no items to record').toBeGreaterThan(0);
117
+ recorded.set(cliWorld.workDir, names);
118
+ },
119
+ );
120
+
121
+ Then(
122
+ 'the context output should show the recorded item names',
123
+ { tags: '@cli' },
124
+ async ({ cliWorld }: { cliWorld: CliWorld }) => {
125
+ const before = recorded.get(cliWorld.workDir);
126
+ expect(before, 'no item names were recorded').toBeTruthy();
127
+ expect(contextItemNames(cliWorld.stdout)).toEqual(before);
128
+ },
129
+ );
130
+
131
+ Then(
132
+ 'the output should list {string} before {string}',
133
+ { tags: '@cli' },
134
+ async ({ cliWorld }: { cliWorld: CliWorld }, first: string, second: string) => {
135
+ const out = cliWorld.stdout;
136
+ const a = out.indexOf(first);
137
+ const b = out.indexOf(second);
138
+ expect(a, `"${first}" is not in the output`).toBeGreaterThanOrEqual(0);
139
+ expect(b, `"${second}" is not in the output`).toBeGreaterThanOrEqual(0);
140
+ expect(a, `"${first}" should come before "${second}"`).toBeLessThan(b);
141
+ },
142
+ );
143
+
144
+ // ── MCP ────────────────────────────────────────────────────────────────
145
+
146
+ Given(
147
+ 'the payments catalogue is installed in the MCP workspace',
148
+ { tags: '@mcp' },
149
+ async ({ mcp }: { mcp: McpPort }) => {
150
+ const workspace = mcp.getWorkspaceDir();
151
+ runKata(`kata tax bundle add -y "${catalogueExample}"`, workspace);
152
+ copyPayments(workspace);
153
+ await mcp.reconnect();
154
+ },
155
+ );
156
+
157
+ Given(
158
+ 'the MCP workspace has a profile {string} that is active',
159
+ { tags: '@mcp' },
160
+ async ({ mcp }: { mcp: McpPort }, name: string) => {
161
+ const workspace = mcp.getWorkspaceDir();
162
+ runKata(`kata config profile create ${name} --copy-from local`, workspace);
163
+ runKata(`kata config profile use ${name}`, workspace);
164
+ await mcp.reconnect();
165
+ },
166
+ );
167
+
168
+ Given(
169
+ 'the context plugin is configured in the MCP workspace with:',
170
+ { tags: '@mcp' },
171
+ async ({ mcp }: { mcp: McpPort }, docString: string) => {
172
+ const workspace = mcp.getWorkspaceDir();
173
+ appendPluginConfig(path.join(`${workspace}-home`, 'config', 'config.yaml'), docString);
174
+ await mcp.reconnect();
175
+ },
176
+ );
177
+
178
+ When(
179
+ 'I record the item names of the MCP context result',
180
+ { tags: '@mcp' },
181
+ async ({ mcp, mcpToolResults }: { mcp: McpPort; mcpToolResults: Map<string, McpToolResult> }) => {
182
+ const result = mcpToolResults.get('_last');
183
+ expect(result, 'no result to record').toBeTruthy();
184
+ expect(result!.isError, result!.text).toBe(false);
185
+ const names = contextItemNames(result!.text);
186
+ expect(names.length, 'the answer has no items to record').toBeGreaterThan(0);
187
+ recorded.set(mcp.getWorkspaceDir(), names);
188
+ },
189
+ );
190
+
191
+ Then(
192
+ 'the markdown of the MCP context result should show the recorded item names',
193
+ { tags: '@mcp' },
194
+ async ({ mcp, mcpToolResults }: { mcp: McpPort; mcpToolResults: Map<string, McpToolResult> }) => {
195
+ const before = recorded.get(mcp.getWorkspaceDir());
196
+ expect(before, 'no item names were recorded').toBeTruthy();
197
+ const result = mcpToolResults.get('_last');
198
+ expect(result, 'no result').toBeTruthy();
199
+ const markdown = (JSON.parse(result!.text) as { data: { markdown?: string } }).data.markdown;
200
+ expect(markdown, 'the result has no markdown').toBeTruthy();
201
+ expect(contextItemNames(markdown!)).toEqual(before);
202
+ },
203
+ );
204
+
205
+ Then(
206
+ 'the MCP context result should show the recorded item names',
207
+ { tags: '@mcp' },
208
+ async ({ mcp, mcpToolResults }: { mcp: McpPort; mcpToolResults: Map<string, McpToolResult> }) => {
209
+ const before = recorded.get(mcp.getWorkspaceDir());
210
+ expect(before, 'no item names were recorded').toBeTruthy();
211
+ const result = mcpToolResults.get('_last');
212
+ expect(result, 'no result').toBeTruthy();
213
+ expect(contextItemNames(result!.text)).toEqual(before);
214
+ },
215
+ );
216
+ }
@@ -0,0 +1,5 @@
1
+ // The context plugin's own steps, on the shared fixtures. The shared steps are registered by xspec/support/steps.ts.
2
+ import { test } from '../../../../xspec/support/fixtures.js';
3
+ import { registerContextSteps } from './context.js';
4
+
5
+ registerContextSteps(test);
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../xspec/tsconfig.json",
3
+ "include": ["steps/**/*.ts", "playwright.config.ts"]
4
+ }