katalyst-taxonomy-cli 2.0.1a2__tar.gz → 2.0.1a4__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 (96) hide show
  1. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/PKG-INFO +8 -1
  2. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/pyproject.toml +13 -1
  3. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/_wiring.py +4 -2
  4. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/app.py +72 -3
  5. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/config_schemas.py +26 -2
  6. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/core.py +121 -0
  7. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/create.py +38 -7
  8. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/init.py +1 -1
  9. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/lifecycle.py +3 -1
  10. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/lint.py +45 -18
  11. katalyst_taxonomy_cli-2.0.1a4/src/katalyst_taxonomy_cli/commands/migrate.py +210 -0
  12. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/plugin.py +17 -2
  13. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/relate.py +31 -5
  14. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/relocate.py +16 -5
  15. katalyst_taxonomy_cli-2.0.1a4/src/katalyst_taxonomy_cli/commands/rescaffold.py +149 -0
  16. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/status.py +45 -0
  17. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/update.py +55 -4
  18. katalyst_taxonomy_cli-2.0.1a4/src/katalyst_taxonomy_cli/fields.py +117 -0
  19. katalyst_taxonomy_cli-2.0.1a4/tests/test_backend_option.py +135 -0
  20. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_cli_version.py +29 -0
  21. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_config_schemas_command.py +37 -3
  22. katalyst_taxonomy_cli-2.0.1a4/tests/test_core_schema.py +226 -0
  23. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_create.py +41 -1
  24. katalyst_taxonomy_cli-2.0.1a4/tests/test_create_owners.py +58 -0
  25. katalyst_taxonomy_cli-2.0.1a4/tests/test_import_inside_repo.py +89 -0
  26. katalyst_taxonomy_cli-2.0.1a4/tests/test_labels_annotations.py +289 -0
  27. katalyst_taxonomy_cli-2.0.1a4/tests/test_lifecycle_kind_selector.py +113 -0
  28. katalyst_taxonomy_cli-2.0.1a4/tests/test_lint_profile_graph.py +155 -0
  29. katalyst_taxonomy_cli-2.0.1a4/tests/test_migrate.py +142 -0
  30. katalyst_taxonomy_cli-2.0.1a4/tests/test_optional_extras.py +69 -0
  31. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_plugin_command.py +32 -1
  32. katalyst_taxonomy_cli-2.0.1a4/tests/test_template_merge.py +201 -0
  33. katalyst_taxonomy_cli-2.0.1a2/src/katalyst_taxonomy_cli/fields.py +0 -70
  34. katalyst_taxonomy_cli-2.0.1a2/tests/test_core_schema.py +0 -95
  35. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/.gitignore +0 -0
  36. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/__init__.py +0 -0
  37. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/__main__.py +0 -0
  38. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/__init__.py +0 -0
  39. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/api.py +0 -0
  40. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/backend.py +0 -0
  41. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/bundle.py +0 -0
  42. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/clone.py +0 -0
  43. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/config.py +0 -0
  44. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/config_filters.py +0 -0
  45. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/config_home.py +0 -0
  46. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/config_plugins.py +0 -0
  47. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/config_profile.py +0 -0
  48. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/config_sources.py +0 -0
  49. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/deps.py +0 -0
  50. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/diff.py +0 -0
  51. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/drafts.py +0 -0
  52. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/export.py +0 -0
  53. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/kinds.py +0 -0
  54. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/promote.py +0 -0
  55. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/query.py +0 -0
  56. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/remove.py +0 -0
  57. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/resolve.py +0 -0
  58. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/rollback.py +0 -0
  59. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/self_update.py +0 -0
  60. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/show.py +0 -0
  61. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/sync.py +0 -0
  62. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/commands/versions.py +0 -0
  63. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/context.py +0 -0
  64. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/discovery.py +0 -0
  65. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/install.py +0 -0
  66. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/options.py +0 -0
  67. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/output.py +0 -0
  68. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/src/katalyst_taxonomy_cli/py.typed +0 -0
  69. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/conftest.py +0 -0
  70. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_add.py +0 -0
  71. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_add_bundle.py +0 -0
  72. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_api_command.py +0 -0
  73. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_clone.py +0 -0
  74. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_command_signatures.py +0 -0
  75. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_config_command.py +0 -0
  76. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_config_explain.py +0 -0
  77. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_config_home.py +0 -0
  78. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_config_plugins_command.py +0 -0
  79. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_config_profiles.py +0 -0
  80. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_config_sources.py +0 -0
  81. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_create_typed_and_variants.py +0 -0
  82. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_deps.py +0 -0
  83. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_git_backend_cli.py +0 -0
  84. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_github_backend_cli.py +0 -0
  85. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_install.py +0 -0
  86. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_kinds.py +0 -0
  87. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_kinds_plugin_settings.py +0 -0
  88. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_lint_output.py +0 -0
  89. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_lint_strict.py +0 -0
  90. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_output.py +0 -0
  91. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_plugin_gating.py +0 -0
  92. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_relate_update.py +0 -0
  93. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_relocate.py +0 -0
  94. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_remove.py +0 -0
  95. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_repository_targets.py +0 -0
  96. {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a4}/tests/test_self_update.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: katalyst-taxonomy-cli
3
- Version: 2.0.1a2
3
+ Version: 2.0.1a4
4
4
  Summary: CLI for katalyst-taxonomy — the kata command
5
5
  Author: eSimplicity
6
6
  License: Proprietary
@@ -8,5 +8,12 @@ Requires-Python: >=3.12
8
8
  Requires-Dist: click>=8.1
9
9
  Requires-Dist: katalyst-core<1,>=0.1
10
10
  Requires-Dist: katalyst-engine<6,>=5
11
+ Requires-Dist: katalyst-plugin-context<1,>=0.2
12
+ Requires-Dist: katalyst-plugin-harness<1,>=0.1
13
+ Requires-Dist: katalyst-plugin-scaffolding<2,>=1
11
14
  Requires-Dist: katalyst-taxonomy-mcp<5,>=4
12
15
  Requires-Dist: katalyst-taxonomy<6,>=5
16
+ Provides-Extra: bard
17
+ Requires-Dist: katalyst-plugin-bard<1,>=0.1; extra == 'bard'
18
+ Provides-Extra: tui
19
+ Requires-Dist: katalyst-taxonomy-tui<3,>=2; extra == 'tui'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "katalyst-taxonomy-cli"
3
- version = "2.0.1a2"
3
+ version = "2.0.1a4"
4
4
  description = "CLI for katalyst-taxonomy — the kata command"
5
5
  license = { text = "Proprietary" }
6
6
  requires-python = ">=3.12"
@@ -15,8 +15,20 @@ dependencies = [
15
15
  "katalyst-engine>=5,<6",
16
16
  "click>=8.1",
17
17
  "katalyst-core>=0.1,<1",
18
+ # The plugins that ship with kata: `kata tax context|harness|add`. Bounded to the
19
+ # config-rooted runtime, like the plugins' own floors on this package (a cycle pip
20
+ # and uv both resolve: a plugin's command imports this package's context and options).
21
+ "katalyst-plugin-context>=0.2,<1",
22
+ "katalyst-plugin-harness>=0.1,<1",
23
+ "katalyst-plugin-scaffolding>=1,<2",
18
24
  ]
19
25
 
26
+ [project.optional-dependencies]
27
+ # `kata tax bard`: the web UI plugin.
28
+ bard = ["katalyst-plugin-bard>=0.1,<1"]
29
+ # `kata tui`: the terminal UI.
30
+ tui = ["katalyst-taxonomy-tui>=2,<3"]
31
+
20
32
  [project.scripts]
21
33
  kata = "katalyst_taxonomy_cli.__main__:main"
22
34
 
@@ -123,7 +123,9 @@ def _named(registry: BackendRegistry, target: str | None, address: str) -> tuple
123
123
  A prefix and ``--backend`` that disagree is an error rather than one silently
124
124
  winning: the command was told two different stores.
125
125
  """
126
- if address:
126
+ from katalyst_taxonomy.config.routing import split_backend_prefix
127
+
128
+ if address and (target is None or split_backend_prefix(address)[0] is not None):
127
129
  parsed = registry.address(address)
128
130
  if target and target != parsed.backend:
129
131
  raise click.ClickException(
@@ -135,7 +137,7 @@ def _named(registry: BackendRegistry, target: str | None, address: str) -> tuple
135
137
  from katalyst_taxonomy.config.routing import UnknownBackendError
136
138
 
137
139
  raise UnknownBackendError(target, registry.profile_backends())
138
- return target, ""
140
+ return target, address
139
141
  return None, ""
140
142
 
141
143
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import importlib.util
5
6
  import subprocess
6
7
  import sys
7
8
  from pathlib import Path
@@ -77,6 +78,42 @@ class KataGroup(click.Group):
77
78
  raise click.ClickException(str(exc)) from exc
78
79
 
79
80
 
81
+ def _missing_extra(name: str) -> str | None:
82
+ """The extra of this package named *name* whose requirements are not installed.
83
+
84
+ A command group a plugin contributes is offered as an extra of the same name
85
+ (``katalyst-taxonomy-cli[<name>]``). Read from this distribution's own metadata, so
86
+ the CLI names no plugin: ``None`` when there is no such extra, or it is installed.
87
+ """
88
+ import importlib.metadata as metadata
89
+ import re
90
+
91
+ try:
92
+ requirements = metadata.requires("katalyst-taxonomy-cli") or []
93
+ except metadata.PackageNotFoundError:
94
+ return None
95
+ marker = re.compile(rf"""extra\s*==\s*['"]{re.escape(name)}['"]""")
96
+ wanted = [r for r in requirements if marker.search(r)]
97
+ if not wanted:
98
+ return None
99
+ for requirement in wanted:
100
+ distribution = re.split(r"[\s;<>=!~\[(]", requirement, maxsplit=1)[0]
101
+ try:
102
+ metadata.distribution(distribution)
103
+ except metadata.PackageNotFoundError:
104
+ return name
105
+ return None
106
+
107
+
108
+ def _not_installed_message(extra: str, label: str) -> str:
109
+ return (
110
+ f"{label} is not installed. Install it with:\n"
111
+ f" pip install 'katalyst-taxonomy-cli[{extra}]'\n"
112
+ f"or, for a uv tool install:\n"
113
+ f" uv tool install 'katalyst-taxonomy-cli[{extra}]'"
114
+ )
115
+
116
+
80
117
  class TaxGroup(click.Group):
81
118
  """The ``tax`` group, which resolves plugin-contributed commands **lazily**.
82
119
 
@@ -114,6 +151,16 @@ class TaxGroup(click.Group):
114
151
  self._ensure_plugin_commands(ctx)
115
152
  return super().get_command(ctx, cmd_name)
116
153
 
154
+ def resolve_command(
155
+ self, ctx: click.Context, args: list[str]
156
+ ) -> tuple[str | None, click.Command | None, list[str]]:
157
+ name = args[0] if args else None
158
+ if name and self.get_command(ctx, name) is None:
159
+ extra = _missing_extra(name)
160
+ if extra is not None:
161
+ raise click.ClickException(_not_installed_message(extra, f"`kata tax {name}`"))
162
+ return super().resolve_command(ctx, args)
163
+
117
164
  def _ensure_plugin_commands(self, ctx: click.Context) -> None:
118
165
  if self._plugin_commands_resolved:
119
166
  return
@@ -159,6 +206,13 @@ def _installed_katalyst_packages() -> list[tuple[str, str]]:
159
206
  return sorted(found.items())
160
207
 
161
208
 
209
+ def _schema_versions() -> list[tuple[str, str]]:
210
+ """Every schema installed (katalyst-core, and each bundle), with its version."""
211
+ from katalyst_engine.evolution import installed_releases
212
+
213
+ return [(release.name, str(release.version)) for release in installed_releases()]
214
+
215
+
162
216
  def _print_version(ctx: click.Context, _param: click.Parameter, value: bool) -> None:
163
217
  if not value or ctx.resilient_parsing:
164
218
  return
@@ -170,6 +224,12 @@ def _print_version(ctx: click.Context, _param: click.Parameter, value: bool) ->
170
224
  click.echo(f"kata {__version__}")
171
225
  for name, version in _installed_katalyst_packages():
172
226
  click.echo(f" {name} {version}")
227
+ click.echo("schemas:")
228
+ try:
229
+ for name, version in _schema_versions():
230
+ click.echo(f" {name} {version}")
231
+ except Exception as exc: # a bundle that does not load must not hide the versions
232
+ click.echo(f" unavailable: {exc}")
173
233
  for line in install_lines():
174
234
  click.echo(line)
175
235
  try:
@@ -227,8 +287,8 @@ def _select_home(ctx: click.Context, _param: click.Parameter, value: str | None)
227
287
  # Not eager, so it runs after --home (which is) and reports the home chosen.
228
288
  callback=_print_version,
229
289
  help=(
230
- "Show the kata version, every installed Katalyst package, the install (and what it "
231
- "was built from) and the home, then exit."
290
+ "Show the kata version, every installed Katalyst package, the schema bundles with "
291
+ "their versions, the install (and what it was built from) and the home, then exit."
232
292
  ),
233
293
  )
234
294
  @click.option(
@@ -295,7 +355,12 @@ def _configure_logging() -> None:
295
355
 
296
356
  @cli.command("tui")
297
357
  def tui_cmd() -> None:
298
- """Launch the interactive terminal UI."""
358
+ """Launch the interactive terminal UI.
359
+
360
+ The UI is an extra: ``pip install 'katalyst-taxonomy-cli[tui]'``.
361
+ """
362
+ if importlib.util.find_spec("katalyst_taxonomy_tui") is None:
363
+ raise click.ClickException(_not_installed_message("tui", "the terminal UI"))
299
364
  try:
300
365
  subprocess.run(
301
366
  [sys.executable, "-m", "katalyst_taxonomy_tui"],
@@ -466,12 +531,14 @@ def _register_core_commands() -> None:
466
531
  kinds,
467
532
  lifecycle,
468
533
  lint,
534
+ migrate,
469
535
  plugin,
470
536
  promote,
471
537
  query,
472
538
  relate,
473
539
  relocate,
474
540
  remove,
541
+ rescaffold,
475
542
  resolve,
476
543
  rollback,
477
544
  self_update,
@@ -505,6 +572,7 @@ def _register_core_commands() -> None:
505
572
  tax.add_command(show.show)
506
573
  tax.add_command(kinds.kinds)
507
574
  tax.add_command(lint.lint)
575
+ tax.add_command(migrate.migrate)
508
576
  tax.add_command(deps.deps)
509
577
  tax.add_command(status.status)
510
578
  tax.add_command(rollback.rollback)
@@ -528,6 +596,7 @@ def _register_core_commands() -> None:
528
596
  tax.add_command(export.export)
529
597
  tax.add_command(backend.backend)
530
598
  tax.add_command(remove.remove)
599
+ tax.add_command(rescaffold.rescaffold)
531
600
 
532
601
 
533
602
  def resolve_plugin_commands() -> None:
@@ -60,7 +60,11 @@ def build_schemas_payload(
60
60
  from katalyst_taxonomy.config.schemas import selection_from_config
61
61
  from katalyst_taxonomy.engine import bridge
62
62
  from katalyst_taxonomy.services.schema_gate import check_declared_bundles
63
- from katalyst_taxonomy.services.schema_report import schemas_payload
63
+ from katalyst_taxonomy.services.schema_report import (
64
+ core_schema,
65
+ schema_versions,
66
+ schemas_payload,
67
+ )
64
68
  from katalyst_taxonomy.sources.template_resolver import TemplateResolver
65
69
 
66
70
  effective = config_resolver().resolve(profile=profile)
@@ -90,6 +94,8 @@ def build_schemas_payload(
90
94
  if candidate.distribution
91
95
  },
92
96
  declared=declared,
97
+ schema_versions=schema_versions(candidates),
98
+ core_schema=core_schema(),
93
99
  scaffold_providers=resolver.scaffold_template_providers(),
94
100
  repo_root=None,
95
101
  filesystem=RealFilesystem(),
@@ -155,19 +161,24 @@ def _render_text(out: OutputPort, payload: dict[str, Any]) -> None:
155
161
  ]
156
162
  )
157
163
 
164
+ core = payload.get("core")
165
+ if core:
166
+ out.key_value([("katalyst-core schema", _schema_text(core))])
167
+
158
168
  out.heading("Bundles")
159
169
  bundles: list[dict[str, Any]] = payload["bundles"]
160
170
  if not bundles:
161
171
  out.info("No schema bundles discovered.")
162
172
  else:
163
173
  out.table(
164
- ["", "bundle", "distribution", "version", "kinds", "note"],
174
+ ["", "bundle", "distribution", "version", "schema", "kinds", "note"],
165
175
  [
166
176
  [
167
177
  "*" if entry["enabled"] else "x",
168
178
  str(entry["name"]),
169
179
  str(entry.get("distribution") or "-"),
170
180
  str(entry.get("resolvedVersion") or entry["version"] or "-"),
181
+ _schema_text(entry.get("schema")),
171
182
  str(len(entry["kinds"])),
172
183
  str(
173
184
  entry["reason"] or entry.get("sourceWarning") or entry["entryPoint"] or "-"
@@ -239,6 +250,19 @@ def _render_text(out: OutputPort, payload: dict[str, Any]) -> None:
239
250
  )
240
251
 
241
252
 
253
+ def _schema_text(schema: dict[str, Any] | None) -> str:
254
+ """``9.0.0 (unreleased; latest archived 8.0.0)``: a schema's version and archive state."""
255
+ if not schema:
256
+ return "-"
257
+ from katalyst_engine.core.version import Version
258
+ from katalyst_engine.evolution import ArchiveState
259
+
260
+ archive = schema["archive"]
261
+ latest = archive["latest"]
262
+ state = ArchiveState(archive["state"], Version.parse(latest) if latest else None)
263
+ return f"{schema['version']} ({state})"
264
+
265
+
242
266
  def attach(group: click.Group) -> None:
243
267
  """Add ``schemas`` to the ``kata config`` group.
244
268
 
@@ -23,6 +23,12 @@
23
23
  the diff
24
24
  kata core schema archive --bundle B
25
25
  archive the current modules as the released version
26
+ kata core schema docs [PATH...] [--bundle B] [--check] [--kind K]
27
+ write the generated field tables of the pages
28
+ under PATH; --check fails when any is stale
29
+ kata core schema changelog [--bundle B] [--from VERSION]
30
+ the "Schema changes" section of a release: what was
31
+ added, what broke, and the migration step for each
26
32
 
27
33
  Records and graphs are read against every installed module (``katalyst_core.modules``).
28
34
  """
@@ -466,3 +472,118 @@ def schema_archive(bundle: str) -> None:
466
472
  except (FileExistsError, SchemaVersionMismatchError) as exc:
467
473
  raise click.ClickException(str(exc)) from exc
468
474
  click.echo(f"Archived {release.name} {release.version} in {target}")
475
+
476
+
477
+ # Named from the function (``docs``), so no command-name literal reads as a bundle name.
478
+ @schema.command()
479
+ @click.argument("paths", nargs=-1, type=click.Path(exists=True, path_type=Path))
480
+ @click.option(
481
+ "--bundle",
482
+ "bundle",
483
+ default=None,
484
+ help="Read kinds from this schema only (needed when two define the same kind).",
485
+ )
486
+ @click.option(
487
+ "--check",
488
+ is_flag=True,
489
+ help="Write nothing; fail when a generated region is stale (for CI).",
490
+ )
491
+ @click.option(
492
+ "--kind",
493
+ "kind",
494
+ default=None,
495
+ help="Print the markdown for one kind (to start a page), and write nothing.",
496
+ )
497
+ def docs(paths: tuple[Path, ...], bundle: str | None, check: bool, kind: str | None) -> None:
498
+ """Write the generated field tables of the markdown pages under PATH.
499
+
500
+ A page marks where a table goes with a line of its own, ``<!-- generated:kind:NAME -->``
501
+ (a kind's parent and fields: type, required, allowed values, references and their
502
+ relationship types, descriptions) or ``<!-- generated:wing:NAME -->`` (a wing's kinds:
503
+ where each is stored and its parent), and closes it with ``<!-- /generated -->``. The
504
+ text between is rewritten from the schemas' LinkML modules; everything else on the page
505
+ is yours.
506
+ """
507
+ from katalyst_engine.schema_docs import (
508
+ DocsError,
509
+ RegionError,
510
+ catalog_from_releases,
511
+ page_files,
512
+ refresh_page,
513
+ render_kind,
514
+ )
515
+
516
+ if kind is None and not paths:
517
+ raise click.UsageError("name the pages to write: one or more files or directories")
518
+ catalog = catalog_from_releases(_releases(bundle))
519
+ try:
520
+ if kind is not None:
521
+ click.echo(render_kind(catalog.kind(kind, bundle=bundle)))
522
+ return
523
+ stale_total = 0
524
+ for page in page_files(paths):
525
+ text = page.read_text(encoding="utf-8")
526
+ try:
527
+ new_text, stale = refresh_page(text, catalog, bundle=bundle)
528
+ except (RegionError, DocsError) as exc:
529
+ raise click.ClickException(f"{page}: {exc}") from exc
530
+ stale_total += len(stale)
531
+ for region in stale:
532
+ click.echo(f"{page}:{region.line}: generated:{region.label} is stale")
533
+ if stale and not check:
534
+ page.write_text(new_text, encoding="utf-8")
535
+ except DocsError as exc:
536
+ raise click.ClickException(str(exc)) from exc
537
+ if check:
538
+ if stale_total:
539
+ raise click.ClickException(
540
+ f"{stale_total} generated region(s) are stale; "
541
+ f"run kata core schema docs {' '.join(str(p) for p in paths)}"
542
+ )
543
+ click.echo("generated regions are current")
544
+ else:
545
+ click.echo(f"Updated {stale_total} generated region(s)")
546
+
547
+
548
+ @schema.command("changelog")
549
+ @_BUNDLE
550
+ @click.option(
551
+ "--from",
552
+ "since",
553
+ default=None,
554
+ help="The archived version to compare with (default: the latest).",
555
+ )
556
+ @click.option(
557
+ "--heading-level",
558
+ "level",
559
+ type=click.IntRange(1, 5),
560
+ default=2,
561
+ show_default=True,
562
+ help="The level of the section's heading (2: ##).",
563
+ )
564
+ def schema_changelog(bundle: str | None, since: str | None, level: int) -> None:
565
+ """The "Schema changes" section of a release, from the classified diff and the migration.
566
+
567
+ For each schema whose modules changed since its last archived version: what was added,
568
+ what broke, and for each breaking change the `kata tax migrate` step that handles it
569
+ (or the reason it is accepted). Markdown on stdout, for the release notes.
570
+ """
571
+ from katalyst_engine.core.version import Version
572
+ from katalyst_engine.evolution import build_changelog, render_changelog
573
+
574
+ logs: list[Any] = []
575
+ for release in _releases(bundle):
576
+ try:
577
+ old = Version.parse(since) if since else None
578
+ log = build_changelog(release, old)
579
+ except (ValueError, FileNotFoundError) as exc:
580
+ raise click.ClickException(str(exc)) from exc
581
+ if log is None:
582
+ click.echo(f"{release.name}: no released version is archived", err=True)
583
+ else:
584
+ logs.append(log)
585
+ text = render_changelog(logs, level=level)
586
+ if not text:
587
+ click.echo("No schema changes since the last released version.", err=True)
588
+ return
589
+ click.echo(text, nl=False)
@@ -41,7 +41,24 @@ from katalyst_taxonomy_cli.context import CliContext
41
41
  "from_file",
42
42
  default=None,
43
43
  metavar="PATH",
44
- help="Spec fields from a YAML or JSON file (- for stdin); --set applies over them.",
44
+ help=(
45
+ "Spec fields from a YAML or JSON file (- for stdin), or a whole document's spec, "
46
+ "labels and annotations; --set, --label and --annotation apply over them."
47
+ ),
48
+ )
49
+ @click.option(
50
+ "--label",
51
+ "label_pairs",
52
+ multiple=True,
53
+ metavar="KEY=VALUE",
54
+ help="Set a label (metadata.labels), which filters and permission grants match. Repeat.",
55
+ )
56
+ @click.option(
57
+ "--annotation",
58
+ "annotation_pairs",
59
+ multiple=True,
60
+ metavar="KEY=VALUE",
61
+ help="Set an annotation (metadata.annotations), a record nothing matches on. Repeat.",
45
62
  )
46
63
  @click.option("--target-dir", default=None, help="Override output directory.")
47
64
  @click.option(
@@ -75,6 +92,8 @@ def create(
75
92
  scaffold_variant: str | None,
76
93
  set_fields: tuple[str, ...],
77
94
  from_file: str | None,
95
+ label_pairs: tuple[str, ...],
96
+ annotation_pairs: tuple[str, ...],
78
97
  target_dir: str | None,
79
98
  backend_target: str | None,
80
99
  dry_run: bool,
@@ -96,24 +115,34 @@ def create(
96
115
  a value of its type (boolean, integer, list, map) and allowed values, a reference
97
116
  naming a node of the referenced kind.
98
117
 
118
+ --label KEY=VALUE and --annotation KEY=VALUE go in the node's metadata, on any kind.
119
+ A key is a name of letters, digits, '-', '_' and '.' (at most 63), optionally after a
120
+ DNS prefix and '/' (example.org/team); a label's value is such a name (or empty), an
121
+ annotation's any text.
122
+
99
123
  Examples:
100
124
 
101
- kata tax create stage verify --set halt_on_failure=false --set concern_order=maintainability
125
+ kata tax create layer api --under shop.backend --set audited=false --set tier=gold
126
+
127
+ kata tax create layer worker --under shop.backend --set priority=10 --set limits.region=eu-west
102
128
 
103
- kata tax create operation probe --set order=10 --set scope.regions=eu-west
129
+ kata tax create capability checkout --under shop --set 'hooks=[{"on":"push","branches":["main"]}]'
104
130
 
105
- kata tax create venue ci --set 'triggers=[{"on":"push","environments":["dev"]}]'
131
+ kata tax create stack web --under shop --set limits.log_level=debug
106
132
 
107
- kata tax create environment qa --set template_replacements.log_level=debug
133
+ kata tax create layer api --under shop.backend --label team=payments --annotation source=docs/api.md
108
134
  """
109
135
  from katalyst_taxonomy.adapters.orchestration import run_create
110
136
  from katalyst_taxonomy.config.routing import split_backend_prefix
111
137
 
112
- from katalyst_taxonomy_cli.fields import parse_set, read_spec_file
138
+ from katalyst_taxonomy_cli.fields import parse_pairs, parse_set, read_node_file
113
139
 
114
140
  from .._wiring import resolve_backend
115
141
 
116
142
  resolved_parent = parent or parent_alt
143
+ node_file = read_node_file(from_file)
144
+ labels = {**node_file.labels, **parse_pairs(label_pairs, option="--label")}
145
+ annotations = {**node_file.annotations, **parse_pairs(annotation_pairs, option="--annotation")}
117
146
 
118
147
  # The backend is `--backend`, or the `backend:` prefix of NAME. Without either, the
119
148
  # repository is the one the current directory is inside — even with one repository,
@@ -134,7 +163,9 @@ def create(
134
163
  description=description,
135
164
  owners=owners or (),
136
165
  fields=parse_set(set_fields),
137
- spec=read_spec_file(from_file),
166
+ spec=node_file.spec,
167
+ labels=labels,
168
+ annotations=annotations,
138
169
  parent=resolved_parent,
139
170
  scaffold_variant=scaffold_variant,
140
171
  target_dir=target_dir,
@@ -66,7 +66,7 @@ def init(
66
66
 
67
67
  Writes the taxonomy root (.global/taxonomy/) and version.yaml — together the
68
68
  "initialized" marker — and registers the repository as a filesystem backend of a
69
- profile in the user config file (<user config home>/config.yaml): the --profile
69
+ profile in the active home's config (`kata config path` prints it): the --profile
70
70
  profile (created when it is not declared), else the active one; a file with no
71
71
  profiles gets a new `local`, made active. The backend is named --backend, else for
72
72
  the directory; a name the profile already declares for another directory is refused
@@ -118,10 +118,12 @@ def import_(ctx: CliContext, file: Path, backend_target: str) -> None:
118
118
  resolved.backend,
119
119
  [cast("dict[str, Any]", d) for d in documents],
120
120
  create=lambda request: create_node(resolved.backend, request),
121
+ source=file,
121
122
  )
122
123
  except ImportError_ as exc:
123
124
  ctx.output.echo(f"Error: {exc}")
124
- ctx.output.echo("Error: nothing was imported.")
125
+ if not exc.left_behind:
126
+ ctx.output.echo("Error: nothing was imported.")
125
127
  raise SystemExit(1) from exc
126
128
  for fqtn in created:
127
129
  ctx.output.echo(f"Imported: {resolved.name}:{fqtn}")
@@ -16,21 +16,39 @@ from katalyst_taxonomy_cli.context import CliContext
16
16
  is_flag=True,
17
17
  help=(
18
18
  "Treat advisory findings (governance id collisions, duplicate names, undeclared "
19
- "spec keys, misplaced documents) as errors."
19
+ "spec keys, misplaced documents, shape findings and shape warnings) as errors."
20
+ ),
21
+ )
22
+ @click.option(
23
+ "--backend",
24
+ default=None,
25
+ help=(
26
+ "Report only this backend's findings. Its documents are still checked against "
27
+ "the graph of every backend the profile reads."
20
28
  ),
21
29
  )
22
30
  @click.pass_obj
23
- def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
31
+ def lint(ctx: CliContext, verbose: bool, strict: bool, backend: str | None) -> None:
24
32
  """Validate taxonomy against schema — everything the active profile reads.
25
33
 
26
- Document checks run over every backend the profile reads. Repository-local checks
27
- (plugin rules, plugin-document schemas, version constraints) run once per
28
- filesystem backend, and each finding is labelled [backend].
34
+ Every check runs over one graph: the documents of every backend the profile reads,
35
+ so a reference into another backend (other:shop.storefront.checkout) is checked
36
+ against the node it names. Each finding is labelled [backend] when the profile
37
+ reads more than one. Repository-local checks (plugin rules, plugin-document schemas,
38
+ version constraints) run once per filesystem backend. A backend that cannot be read
39
+ is an error naming it.
29
40
  """
30
41
  from katalyst_taxonomy.adapters.orchestration import run_lint
42
+ from katalyst_taxonomy.config.routing import BackendUnavailableError
31
43
 
32
44
  try:
33
- result = run_lint(profile=ctx.profile)
45
+ result = run_lint(profile=ctx.profile, backend=backend)
46
+ except BackendUnavailableError as exc:
47
+ raise click.ClickException(
48
+ f"{exc}\nLint checks the graph of every backend the profile reads, so each "
49
+ "must be readable: make it reachable, or lint with a profile that does not "
50
+ "read it."
51
+ ) from exc
34
52
  except ValueError as exc:
35
53
  # A `filters` block that cannot be honoured — `include: []`, a clause
36
54
  # declaring `match.backend` — is a user error (#217), and lint does not go
@@ -39,6 +57,17 @@ def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
39
57
  # it still reaches the root group and still exits 7.
40
58
  raise click.ClickException(str(exc)) from exc
41
59
 
60
+ labelled = len(result.graph_backends) > 1
61
+
62
+ def _where(path: object) -> str:
63
+ """``[backend] path: `` for a finding about a file, as much as is known."""
64
+ label = ""
65
+ if labelled and isinstance(path, Path):
66
+ owner = result.backend_of(path)
67
+ label = f"[{owner}] " if owner else ""
68
+ shown = f"{path}: " if path is not None and str(path) not in ("", ".") else ""
69
+ return f"{label}{shown}"
70
+
42
71
  # Version constraint warnings are advisory (they do not make the taxonomy
43
72
  # invalid) but must still be surfaced — previously they were unreachable
44
73
  # when the taxonomy was otherwise valid, and only shown under --verbose.
@@ -99,21 +128,15 @@ def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
99
128
  ("Misplaced documents", result.misplaced_documents),
100
129
  ("Relationships no kind carries", result.uncarried_relationships),
101
130
  ("Shape findings", result.shape_findings),
131
+ ("Shape warnings", result.shape_warnings),
102
132
  ):
103
133
  if not found:
104
134
  continue
105
135
  ctx.output.echo(f"\n{title} ({len(found)}):")
106
136
  for violation in found:
107
- where = f"{violation.path}: " if str(violation.path) not in ("", ".") else ""
108
- ctx.output.echo(f" {severity}: {where}{violation.message}")
137
+ ctx.output.echo(f" {severity}: {_where(violation.path)}{violation.message}")
109
138
  if not strict:
110
139
  ctx.output.echo(" Re-run with --strict to fail on these.")
111
- # A shape's warnings (e.g. coverage) are a report, strict or not.
112
- if result.shape_warnings:
113
- ctx.output.echo(f"\nShape warnings ({len(result.shape_warnings)}):")
114
- for violation in result.shape_warnings:
115
- where = f"{violation.path}: " if str(violation.path) not in ("", ".") else ""
116
- ctx.output.echo(f" WARNING: {where}{violation.message}")
117
140
 
118
141
  def _print_history_notes() -> None:
119
142
  found = result.removed_history_targets
@@ -121,8 +144,7 @@ def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
121
144
  return
122
145
  ctx.output.echo(f"\nHistory references to removed nodes ({len(found)}):")
123
146
  for violation in found:
124
- where = f"{violation.path}: " if str(violation.path) not in ("", ".") else ""
125
- ctx.output.echo(f" INFO: {where}{violation.message}")
147
+ ctx.output.echo(f" INFO: {_where(violation.path)}{violation.message}")
126
148
 
127
149
  strict_findings = (
128
150
  len(result.governance_id_collisions)
@@ -131,6 +153,7 @@ def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
131
153
  + len(result.misplaced_documents)
132
154
  + len(result.uncarried_relationships)
133
155
  + len(result.shape_findings)
156
+ + len(result.shape_warnings)
134
157
  )
135
158
  strict_failure = strict and strict_findings > 0
136
159
 
@@ -142,6 +165,10 @@ def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
142
165
  for repo in result.repositories
143
166
  )
144
167
  ctx.output.echo(f" Repositories checked: {repositories or '(none)'}")
168
+ if result.focus is not None:
169
+ ctx.output.echo(
170
+ f" Findings for: {result.focus}; graph: {', '.join(result.graph_backends)}"
171
+ )
145
172
  # Show the receipts: a gate that verified nothing must not look identical
146
173
  # to one that verified everything.
147
174
  ctx.output.echo(
@@ -179,14 +206,14 @@ def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
179
206
  if result.document_errors:
180
207
  ctx.output.echo(f"\nDocument errors ({len(result.document_errors)}):")
181
208
  for err in result.document_errors:
182
- ctx.output.echo(f" {err.path}: {err.message}")
209
+ ctx.output.echo(f" {_where(err.path)}{err.message}")
183
210
 
184
211
  # Positioned between document and plugin-schema errors so block order is
185
212
  # fixed across runs and corpora.
186
213
  if result.reference_errors:
187
214
  ctx.output.echo(f"\nReference errors ({len(result.reference_errors)}):")
188
215
  for ref_err in result.reference_errors:
189
- ctx.output.echo(f" {ref_err.path}: {ref_err.message}")
216
+ ctx.output.echo(f" {_where(ref_err.path)}{ref_err.message}")
190
217
 
191
218
  if result.plugin_schema_errors:
192
219
  ctx.output.echo(f"\nSchema errors ({len(result.plugin_schema_errors)}):")