CLI and API polish

This commit is contained in:
2026-05-04 21:56:19 +02:00
parent 6744cc66e9
commit 539bb9b754
15 changed files with 2401 additions and 2 deletions

View File

@@ -48,6 +48,8 @@ from markitect_tool.cache import (
from markitect_tool.backend import (
BACKEND_CAPABILITIES,
DEFAULT_BACKEND_PATHS,
DEFAULT_LOCAL_INDEX_PATH,
LOCAL_INDEX_SCHEMA_VERSION,
AccessPolicyGateway,
BackendCapabilityCheck,
BackendManifest,
@@ -58,6 +60,9 @@ from markitect_tool.backend import (
DocumentSnapshot,
EMPTY_PARSE_OPTIONS_HASH,
IndexBackend,
LocalIndexBuildResult,
LocalSearchResult,
LocalSnapshotStore,
ProcessorResultStore,
ProvenanceEnvelope,
QueryAdapter,
@@ -70,6 +75,7 @@ from markitect_tool.backend import (
load_backend_manifest,
load_backend_registry,
load_snapshot_state_file,
local_index_path_for,
plan_snapshot_refresh,
snapshot_identity_for_file,
)
@@ -82,6 +88,24 @@ from markitect_tool.content_class import (
load_content_classes,
)
from markitect_tool.diagnostics import Diagnostic, SourceLocation
from markitect_tool.extension import (
ExtensionDependencyCheck,
ExtensionDescriptor,
ExtensionExecutor,
ExtensionLifecycle,
ExtensionRegistry,
ExtensionRegistryError,
ExtensionRunner,
OptionalDependency,
ProcessingCapability,
ProcessingContext,
ProcessingDiagnostic,
ProcessingProvenance,
ProcessingRequest,
ProcessingResult,
ProcessingTrace,
builtin_extension_registry,
)
from markitect_tool.explode import (
EXPLODE_MANIFEST_NAME,
ExplodeEntry,
@@ -157,9 +181,24 @@ from markitect_tool.processor import (
run_fenced_processors,
)
from markitect_tool.policy import (
DecisionLogStore,
DirectoryGroupResolution,
DirectoryGroupResolutionRequest,
DirectoryGroupResolver,
EnterpriseIdentity,
EnterprisePolicyError,
EnterprisePolicyMap,
EnterprisePolicyMapRequest,
EnterprisePolicyMapper,
FlexAuthResource,
FlexAuthResourceManifest,
IdentityClaimsAdapter,
LocalLabelPolicy,
LocalLabelPolicyGateway,
LocalPathPolicyRule,
LocalDecisionLogStore,
LocalEnterprisePolicyMapper,
NetKingdomIdentityClaimsAdapter,
PolicyDecision,
PolicyFilterResult,
PolicyObject,
@@ -168,13 +207,23 @@ from markitect_tool.policy import (
RelationshipPolicyRequest,
RulePolicyAdapter,
RulePolicyRequest,
StaticDirectoryGroupResolver,
load_enterprise_identity_file,
load_enterprise_policy_subject,
policy_metadata_from_document,
)
from markitect_tool.query import (
InvalidQueryError,
QueryEngine,
QueryEngineRegistry,
QueryMatch,
default_query_engine_registry,
extract_document,
extract_document_jsonpath,
extract_document_with_engine,
query_document,
query_document_jsonpath,
query_document_with_engine,
)
from markitect_tool.reference import (
ContentUnit,
@@ -211,6 +260,7 @@ from markitect_tool.schema import (
load_schema_file,
validate_document,
validate_markdown_file,
validate_schema,
)
from markitect_tool.template import (
MissingTemplateVariable,
@@ -244,6 +294,7 @@ __all__ = [
"load_schema_file",
"validate_document",
"validate_markdown_file",
"validate_schema",
"ContractCheckResult",
"ContractValidationResult",
"DocumentContract",
@@ -276,6 +327,8 @@ __all__ = [
"scan_markdown_files",
"BACKEND_CAPABILITIES",
"DEFAULT_BACKEND_PATHS",
"DEFAULT_LOCAL_INDEX_PATH",
"LOCAL_INDEX_SCHEMA_VERSION",
"AccessPolicyGateway",
"BackendCapabilityCheck",
"BackendManifest",
@@ -286,6 +339,9 @@ __all__ = [
"DocumentSnapshot",
"EMPTY_PARSE_OPTIONS_HASH",
"IndexBackend",
"LocalIndexBuildResult",
"LocalSearchResult",
"LocalSnapshotStore",
"ProcessorResultStore",
"ProvenanceEnvelope",
"QueryAdapter",
@@ -298,6 +354,7 @@ __all__ = [
"load_backend_manifest",
"load_backend_registry",
"load_snapshot_state_file",
"local_index_path_for",
"plan_snapshot_refresh",
"snapshot_identity_for_file",
"ClassCompositionResult",
@@ -308,6 +365,22 @@ __all__ = [
"load_content_classes",
"Diagnostic",
"SourceLocation",
"ExtensionDependencyCheck",
"ExtensionDescriptor",
"ExtensionExecutor",
"ExtensionLifecycle",
"ExtensionRegistry",
"ExtensionRegistryError",
"ExtensionRunner",
"OptionalDependency",
"ProcessingCapability",
"ProcessingContext",
"ProcessingDiagnostic",
"ProcessingProvenance",
"ProcessingRequest",
"ProcessingResult",
"ProcessingTrace",
"builtin_extension_registry",
"EXPLODE_MANIFEST_NAME",
"ExplodeEntry",
"ExplodeError",
@@ -370,9 +443,24 @@ __all__ = [
"default_processor_registry",
"discover_fenced_processors",
"run_fenced_processors",
"DecisionLogStore",
"DirectoryGroupResolution",
"DirectoryGroupResolutionRequest",
"DirectoryGroupResolver",
"EnterpriseIdentity",
"EnterprisePolicyError",
"EnterprisePolicyMap",
"EnterprisePolicyMapRequest",
"EnterprisePolicyMapper",
"FlexAuthResource",
"FlexAuthResourceManifest",
"IdentityClaimsAdapter",
"LocalLabelPolicy",
"LocalLabelPolicyGateway",
"LocalPathPolicyRule",
"LocalDecisionLogStore",
"LocalEnterprisePolicyMapper",
"NetKingdomIdentityClaimsAdapter",
"PolicyDecision",
"PolicyFilterResult",
"PolicyObject",
@@ -381,11 +469,21 @@ __all__ = [
"RelationshipPolicyRequest",
"RulePolicyAdapter",
"RulePolicyRequest",
"StaticDirectoryGroupResolver",
"load_enterprise_identity_file",
"load_enterprise_policy_subject",
"policy_metadata_from_document",
"InvalidQueryError",
"QueryEngine",
"QueryEngineRegistry",
"QueryMatch",
"default_query_engine_registry",
"extract_document",
"extract_document_jsonpath",
"extract_document_with_engine",
"query_document",
"query_document_jsonpath",
"query_document_with_engine",
"ContentUnit",
"ReferenceAddress",
"ReferenceContext",

View File

@@ -3,10 +3,12 @@
from __future__ import annotations
import json
import inspect
from pathlib import Path
import click
import yaml
from click.shell_completion import get_completion_class
from markitect_tool.cache import (
build_cache,
@@ -43,7 +45,12 @@ from markitect_tool.document_function import (
render_document_functions,
validate_document_functions,
)
from markitect_tool.extension import ProcessingContext
from markitect_tool.cli.extensions import collect_cli_command_specs
from markitect_tool.extension import (
ExtensionRegistryError,
ProcessingContext,
builtin_extension_registry,
)
from markitect_tool.explode import (
ExplodeError,
explode_markdown_file,
@@ -107,6 +114,118 @@ def main() -> None:
"""Markdown-native toolkit for structured knowledge artifacts."""
@main.command("completion")
@click.argument("shell", type=click.Choice(["bash", "zsh", "fish"], case_sensitive=False))
@click.option(
"--program-name",
default="mkt",
show_default=True,
help="Installed executable name used by the shell completion script.",
)
@click.option(
"--instructions",
is_flag=True,
help="Print installation instructions instead of the completion script.",
)
def completion(shell: str, program_name: str, instructions: bool) -> None:
"""Generate shell completion for Bash, Zsh, or Fish."""
normalized_shell = shell.lower()
if instructions:
click.echo(_completion_instructions(normalized_shell, program_name))
return
completion_class = get_completion_class(normalized_shell)
if completion_class is None:
raise click.ClickException(f"Unsupported completion shell `{shell}`")
complete = completion_class(main, {}, program_name, "_MKT_COMPLETE")
click.echo(complete.source())
@main.group("extension")
def extension_group() -> None:
"""Inspect built-in extension descriptors and CLI affordances."""
@extension_group.command("list")
@click.option("--kind", help="Only show extensions of this kind.")
@click.option(
"--format",
"output_format",
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
default="text",
show_default=True,
)
def extension_list(kind: str | None, output_format: str) -> None:
"""List built-in extension descriptors."""
registry = builtin_extension_registry()
extensions = [descriptor.to_dict() for descriptor in registry.list(kind=kind)]
_emit_extension_catalog({"count": len(extensions), "extensions": extensions}, output_format)
@extension_group.command("inspect")
@click.argument("extension_id")
@click.option(
"--format",
"output_format",
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
default="text",
show_default=True,
)
def extension_inspect(extension_id: str, output_format: str) -> None:
"""Inspect one built-in extension descriptor."""
try:
descriptor = builtin_extension_registry().get(extension_id)
except ExtensionRegistryError as exc:
raise click.ClickException(str(exc)) from exc
_emit_extension_catalog({"extension": descriptor.to_dict()}, output_format)
@extension_group.command("commands")
@click.option(
"--format",
"output_format",
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
default="text",
show_default=True,
)
def extension_commands(output_format: str) -> None:
"""List CLI commands declared by built-in extension descriptors."""
specs = [spec.to_dict() for spec in collect_cli_command_specs(builtin_extension_registry())]
_emit_extension_catalog({"count": len(specs), "commands": specs}, output_format)
@main.group("docs")
def docs_group() -> None:
"""Generate CLI and API reference documentation."""
@docs_group.command("cli")
@click.option(
"--output",
type=click.Path(dir_okay=False, path_type=Path),
help="Write generated Markdown reference to this file.",
)
def docs_cli(output: Path | None) -> None:
"""Generate a Markdown command reference from the Click command tree."""
_emit_or_write_text(_generate_cli_reference(), output)
@docs_group.command("api")
@click.option(
"--output",
type=click.Path(dir_okay=False, path_type=Path),
help="Write generated Markdown reference to this file.",
)
def docs_api(output: Path | None) -> None:
"""Generate a compact Markdown API reference from public exports."""
_emit_or_write_text(_generate_api_reference(), output)
@main.command()
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option(
@@ -2743,6 +2862,36 @@ def _emit_tangle_result(data: dict, output_format: str) -> None:
click.echo(f"written: {written}")
def _emit_extension_catalog(data: dict, output_format: str) -> None:
if output_format == "json":
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
elif output_format == "yaml":
click.echo(yaml.safe_dump(data, sort_keys=False))
elif "extension" in data:
extension = data["extension"]
click.echo(f"{extension['id']} ({extension['kind']})")
if extension.get("summary"):
click.echo(extension["summary"])
commands = extension.get("cli", {}).get("commands", [])
if commands:
click.echo("commands:")
for command in commands:
click.echo(f"- {command}")
docs = extension.get("docs", [])
if docs:
click.echo("docs:")
for doc in docs:
click.echo(f"- {doc}")
elif "commands" in data:
click.echo(f"command specs: {data['count']}")
for spec in data["commands"]:
click.echo(f"- {spec['command']} [{spec['extension_id']}]")
else:
click.echo(f"extensions: {data['count']}")
for extension in data["extensions"]:
click.echo(f"- {extension['id']} ({extension['kind']})")
def _emit_jsonish(data: dict, output_format: str) -> None:
if output_format == "yaml":
click.echo(yaml.safe_dump(data, sort_keys=False))
@@ -2764,6 +2913,115 @@ def _emit_template_analysis(data: dict, output_format: str) -> None:
click.echo(f"! {error}")
def _emit_or_write_text(text: str, output: Path | None) -> None:
if output:
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(text, encoding="utf-8")
else:
click.echo(text)
def _completion_instructions(shell: str, program_name: str) -> str:
command = f"_MKT_COMPLETE={shell}_source {program_name}"
if shell == "fish":
target = f"~/.config/fish/completions/{program_name}.fish"
return (
f"Generate completion script:\n{command} > {target}\n\n"
f"Or load it for the current shell:\n{command} | source"
)
target = f"~/.{program_name}-complete.{shell}"
rc_file = "~/.bashrc" if shell == "bash" else "~/.zshrc"
return (
f"Generate completion script:\n{command} > {target}\n\n"
f"Then add this to {rc_file}:\n. {target}"
)
def _generate_cli_reference() -> str:
lines = [
"# Markitect CLI Reference",
"",
"Generated from the Click command tree.",
"",
]
_append_command_reference(lines, main, "mkt")
return "\n".join(lines).rstrip() + "\n"
def _append_command_reference(lines: list[str], command: click.Command, path: str) -> None:
lines.extend([f"## `{path}`", ""])
if command.help:
lines.extend([command.help, ""])
with click.Context(command, info_name=path.split()[-1]) as ctx:
usage = command.get_usage(ctx).replace("Usage: ", "", 1)
lines.extend(["```text", usage, "```", ""])
params = [
param
for param in command.params
if not getattr(param, "hidden", False) and getattr(param, "name", None)
]
if params:
lines.extend(["Parameters:", ""])
for param in params:
if isinstance(param, click.Option):
names = ", ".join(param.opts + param.secondary_opts)
else:
names = param.human_readable_name
help_text = getattr(param, "help", None) or ""
if param.required:
help_text = (help_text + " Required.").strip()
lines.append(f"- `{names}` - {help_text}".rstrip())
lines.append("")
if isinstance(command, click.Group):
for name, subcommand in sorted(command.commands.items()):
_append_command_reference(lines, subcommand, f"{path} {name}")
def _generate_api_reference() -> str:
import markitect_tool as api
lines = [
"# Markitect API Reference",
"",
"Generated from `markitect_tool.__all__`.",
"",
]
grouped: dict[str, list[str]] = {}
for name in sorted(api.__all__):
obj = getattr(api, name, None)
module = getattr(obj, "__module__", "markitect_tool")
grouped.setdefault(module, []).append(name)
for module in sorted(grouped):
lines.extend([f"## `{module}`", ""])
for name in grouped[module]:
obj = getattr(api, name, None)
lines.append(_api_reference_line(name, obj))
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def _api_reference_line(name: str, obj: object) -> str:
kind = "object"
if inspect.isclass(obj):
kind = "class"
elif inspect.isfunction(obj):
kind = "function"
elif inspect.isbuiltin(obj):
kind = "builtin"
signature = ""
if callable(obj):
try:
signature = str(inspect.signature(obj))
except (TypeError, ValueError):
signature = ""
summary = ""
doc = inspect.getdoc(obj)
if doc:
summary = doc.splitlines()[0]
display = f"`{name}{signature}`" if signature else f"`{name}`"
return f"- {display} - {kind}. {summary}".rstrip()
def _parse_key_value_options(items: tuple[str, ...]) -> dict[str, object]:
values: dict[str, object] = {}
for item in items:

View File

@@ -12,6 +12,8 @@ def builtin_extension_registry() -> ExtensionRegistry:
registry = default_query_engine_registry().extension_registry()
for descriptor in _processor_descriptors() + [
_extension_catalog_descriptor(),
_generated_docs_descriptor(),
_local_sqlite_backend_descriptor(),
_workflow_engine_descriptor(),
_runtime_context_descriptor(),
@@ -25,6 +27,51 @@ def builtin_extension_registry() -> ExtensionRegistry:
return registry
def _extension_catalog_descriptor() -> ExtensionDescriptor:
return ExtensionDescriptor(
id="extension.catalog",
kind="extension-registry",
summary="Inspectable catalog of built-in extension descriptors and CLI affordances.",
capabilities=[
ProcessingCapability(id="extensions", kind="inspect"),
ProcessingCapability(id="cli", kind="inspect"),
],
safety={"reads_files": False, "writes_files": False, "network": False},
input_contract="Built-in ExtensionRegistry",
output_contract="ExtensionDescriptor catalog | CliCommandSpec list",
diagnostics_namespace="extension",
provenance_prefix="extension.catalog",
cli={
"commands": [
"mkt extension list",
"mkt extension inspect",
"mkt extension commands",
]
},
docs=["docs/internal-extension-framework.md"],
)
def _generated_docs_descriptor() -> ExtensionDescriptor:
return ExtensionDescriptor(
id="docs.generated-reference",
kind="documentation",
summary="Generated CLI and API reference documentation from live command and API surfaces.",
capabilities=[
ProcessingCapability(id="docs", kind="generate"),
ProcessingCapability(id="cli", kind="inspect"),
ProcessingCapability(id="api", kind="inspect"),
],
safety={"reads_files": False, "writes_files": True, "network": False},
input_contract="Click command tree | markitect_tool.__all__",
output_contract="Markdown reference document",
diagnostics_namespace="docs",
provenance_prefix="docs.generated_reference",
cli={"commands": ["mkt docs cli", "mkt docs api"]},
docs=["docs/cli-reference.md", "docs/api-reference.md"],
)
def _processor_descriptors() -> list[ExtensionDescriptor]:
return [
ExtensionDescriptor(