From c3b1c88ef0b93a220afbf69d2931a3c52a5fc19f Mon Sep 17 00:00:00 2001 From: CoderDeltaLAN Date: Fri, 19 Jun 2026 00:58:15 +0100 Subject: [PATCH] feat: add explain baseline command --- CHANGELOG.md | 1 + docs/EXIT-CODES.md | 30 ++++--- docs/OUTPUTS.md | 38 ++++----- src/agent_rules_kit/cli.py | 67 ++++++++++++++++ src/agent_rules_kit/explain.py | 139 +++++++++++++++++++++++++++++++++ tests/test_cli.py | 75 ++++++++++++++++++ tests/test_golden_outputs.py | 41 ++++++++++ 7 files changed, 356 insertions(+), 35 deletions(-) create mode 100644 src/agent_rules_kit/explain.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7338310..b13445e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ This project has a published GitHub Release line, but no stable support or API g - Added a CLI contract regression matrix for current version, help, `check`, and `init` output channels and exit codes. - Added the read-only `doctor` baseline command for repository-level instruction diagnosis summaries. - Added the read-only `budget` baseline command for deterministic local instruction-file size metrics. +- Added the read-only `explain` baseline command for local governance rule explanations. ## [0.2.3] - 2026-06-18 diff --git a/docs/EXIT-CODES.md b/docs/EXIT-CODES.md index 2d1a8c8..1bc2b56 100644 --- a/docs/EXIT-CODES.md +++ b/docs/EXIT-CODES.md @@ -67,6 +67,20 @@ Notes: - `budget` does not perform tokenizer-specific counting, remote tokenization, LLM calls, pricing estimates, or optimization claims. - `Approximate words` is not a model token count. +### `explain` + +| Condition | Exit code | Stdout | Stderr | +| --- | ---: | --- | --- | +| Known rules listed or known rule explained | `0` | Console rule explanation output | Empty unless lower-level runtime fails unexpectedly | +| Unknown rule ID, conflicting input, missing input, or command-line usage error | `2` | Empty or argparse-dependent | Error message or argparse-dependent | + +Notes: + +- `explain` is read-only. +- `explain` uses local rule metadata only. +- `explain` does not fetch external documentation, call an LLM, infer new rules, or generate free-form policy advice. +- Unknown rule IDs fail predictably. + ### `init --dry-run` | Condition | Exit code | Stdout | Stderr | @@ -96,18 +110,7 @@ Notes: ## Planned v0.3 exit-code direction -The following commands are not implemented yet. Their exit-code contracts are design targets for future implementation phases. - -### `explain` - -Planned direction: - -| Condition | Exit code | -| --- | ---: | -| Known rule explained or known rules listed | `0` | -| Unknown rule ID, invalid input, or command-line usage error | `2` | - -Unknown rule IDs should fail predictably. They should not silently produce generic guidance. +No remaining v0.3 command exit-code target is documented here after the `explain` baseline. Future release or documentation phases must update this file only from verified behavior. ## Test evidence @@ -124,6 +127,9 @@ The contract regression matrix currently checks: - `doctor` exits `1` when no supported instruction files are found; - `budget` exits `0` when supported instruction files are found; - `budget` exits `1` when no supported instruction files are found; +- `explain --list` exits `0`; +- `explain RULE_ID` exits `0` for known rule IDs; +- `explain RULE_ID` exits `2` for unknown rule IDs; - `init --dry-run` exits `0`; - `init` without `--dry-run` or `--write` exits `2` and writes the supported error to stderr. diff --git a/docs/OUTPUTS.md b/docs/OUTPUTS.md index 1247ba3..8e34cb1 100644 --- a/docs/OUTPUTS.md +++ b/docs/OUTPUTS.md @@ -13,13 +13,10 @@ Implemented command surface: - `agent-rules-kit init --dry-run`; - `agent-rules-kit init --write`; - `agent-rules-kit doctor`; -- `agent-rules-kit budget`. - -Planned v0.3 command surface: - +- `agent-rules-kit budget`; - `agent-rules-kit explain`. -`doctor` and `budget` are implemented as v0.3 command baselines. The remaining planned command is not implemented yet. Its output contract is a design target for a future phase and must not be documented as available behavior until its implementation phase is merged. +`doctor`, `budget`, and `explain` are implemented as v0.3 command baselines. Release preparation remains a separate phase and must not imply tag, release, or PyPI publication until those phases are completed. ## Contract status @@ -47,7 +44,7 @@ That test module currently pins representative exact output for: - `init --dry-run` console output for an existing root `AGENTS.md`; - `init` missing-mode stderr behavior. -It also includes a contract regression matrix for current version, no-command help, `check` console/JSON/Markdown success and no-result behavior, `init --dry-run`, and missing-mode `init` behavior. +It also includes a contract regression matrix for current version, no-command help, `check` console/JSON/Markdown success and no-result behavior, `doctor`, `budget`, `explain`, `init --dry-run`, and missing-mode `init` behavior. This is regression evidence for implemented behavior on current `main`. It is not a stable public API guarantee before v1.0. @@ -74,7 +71,7 @@ Future behavior should preserve that distinction unless a dedicated phase change | `init --write` | console | yes | Explicit write mode with backup behavior for existing root `AGENTS.md`. | | `doctor` | console | yes | Read-only repository-level diagnosis summary. | | `budget` | console | yes | Read-only local size and context-pressure approximation. | -| `explain` | to be defined | no | Planned v0.3 local rule explanation command. | +| `explain` | console | yes | Read-only local governance rule explanation command. | ## Exit codes @@ -95,6 +92,8 @@ Summary for current implemented commands: | `budget` | `0` | Budget approximation completed for supported instruction files. | | `budget` | `1` | No supported instruction files were found. | | `budget` | `2` | Invalid repository input, unsupported instruction-file input, or command-line usage error. | +| `explain` | `0` | Known rule listed or explained. | +| `explain` | `2` | Unknown rule ID, conflicting input, missing input, or command-line usage error. | ## JSON contract for `check` @@ -246,28 +245,21 @@ Current `budget` exit-code behavior: `Approximate words` is a local whitespace-based approximation, not a model token count. -## Planned v0.3 command contracts - -The remaining command is a design target. It is not available until its dedicated implementation phase is merged. - -### `explain` +## Explain output contract -Planned purpose: +Current `explain` console output includes either: -- explain known rule IDs and their limits from local rule metadata or documentation-backed text; -- optionally list known rules. +- a list of known rule IDs with category and title; +- one known rule explanation with title, category, summary, and limits. -Planned output direction: +Current `explain` exit-code behavior: -- local explanations only; -- no external documentation fetch; -- no LLM-generated explanations; -- unsupported rule IDs must fail predictably. +- `0`: known rules listed or a known rule was explained; +- `2`: unknown rule ID, conflicting input, missing input, or command-line usage error. -Planned exit-code direction: +`explain` is read-only. It uses local rule metadata only. It does not fetch external documentation, call an LLM, infer new rules, or generate free-form policy advice. -- `0`: explanation or rule list completed; -- `2`: unknown rule ID, invalid input, or command-line usage error. +Unsupported rule IDs fail predictably instead of producing generic guidance. ## Redaction expectations diff --git a/src/agent_rules_kit/cli.py b/src/agent_rules_kit/cli.py index 521c388..ac462d4 100644 --- a/src/agent_rules_kit/cli.py +++ b/src/agent_rules_kit/cli.py @@ -11,6 +11,11 @@ from agent_rules_kit import __version__ from agent_rules_kit.budget import BudgetReport, build_budget_report from agent_rules_kit.discovery import InstructionFile, discover_instruction_files +from agent_rules_kit.explain import ( + RuleExplanation, + get_rule_explanation, + list_rule_explanations, +) from agent_rules_kit.findings import Finding from agent_rules_kit.governance import find_governance_findings from agent_rules_kit.init_plan import InitPlan, build_init_plan @@ -94,6 +99,22 @@ def build_parser() -> argparse.ArgumentParser: help="Repository root to inspect. Defaults to the current directory.", ) + explain_parser = subparsers.add_parser( + "explain", + help="Explain known governance rule IDs.", + ) + explain_parser.add_argument( + "rule_id", + nargs="?", + help="Known rule ID to explain, such as AIRK-GOV003.", + ) + explain_parser.add_argument( + "--list", + action="store_true", + dest="list_rules", + help="List known rule IDs.", + ) + return parser @@ -122,10 +143,56 @@ def main(argv: Sequence[str] | None = None) -> int: if args.command == "budget": return _run_budget(Path(args.repository)) + if args.command == "explain": + return _run_explain(args.rule_id, list_rules=args.list_rules) + parser.print_help() return 0 +def _run_explain(rule_id: str | None, *, list_rules: bool) -> int: + if list_rules and rule_id is not None: + print("ERROR: explain accepts either --list or a rule ID.", file=sys.stderr) + return 2 + + if list_rules: + _print_known_rules() + return 0 + + if rule_id is None: + print("ERROR: explain requires --list or RULE_ID.", file=sys.stderr) + return 2 + + explanation = get_rule_explanation(rule_id) + if explanation is None: + print( + f"ERROR: unknown rule ID: {redact_secret_like_values(rule_id)}", + file=sys.stderr, + ) + return 2 + + _print_rule_explanation(explanation) + return 0 + + +def _print_known_rules() -> None: + print("agent-rules-kit explain") + print("Known rules:") + for explanation in list_rule_explanations(): + print( + f"- {explanation.rule_id} [{explanation.category}] " + f"{explanation.title}" + ) + + +def _print_rule_explanation(explanation: RuleExplanation) -> None: + print(f"agent-rules-kit explain: {explanation.rule_id}") + print(f"Title: {explanation.title}") + print(f"Category: {explanation.category}") + print(f"Summary: {explanation.summary}") + print(f"Limits: {explanation.limits}") + + def _run_budget(repository_root: Path) -> int: try: instruction_files = discover_instruction_files(repository_root) diff --git a/src/agent_rules_kit/explain.py b/src/agent_rules_kit/explain.py new file mode 100644 index 0000000..0ed7e1d --- /dev/null +++ b/src/agent_rules_kit/explain.py @@ -0,0 +1,139 @@ +"""Local governance rule explanations.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class RuleExplanation: + """Human-readable explanation for one known rule.""" + + rule_id: str + title: str + category: str + summary: str + limits: str + + +RULE_EXPLANATIONS: tuple[RuleExplanation, ...] = ( + RuleExplanation( + rule_id="AIRK-SYS001", + title="Unreadable instruction file", + category="system", + summary=( + "Flags supported instruction files that cannot be analyzed as UTF-8." + ), + limits="Does not print raw undecodable bytes and does not repair encoding.", + ), + RuleExplanation( + rule_id="AIRK-SYS002", + title="Symlinked instruction file", + category="system", + summary=( + "Flags supported instruction file paths that are symlinks and are not " + "analyzed." + ), + limits="Does not follow symlinked instruction files or wildcard directories.", + ), + RuleExplanation( + rule_id="AIRK-GOV006", + title="Unsupported security or maturity claim", + category="governance", + summary=( + "Flags instruction text that appears to claim unsupported safety, " + "security, production readiness, completeness, or maturity." + ), + limits=( + "Does not prove a claim false and does not replace human release " + "review." + ), + ), + RuleExplanation( + rule_id="AIRK-GOV003", + title="Review or CI bypass guidance", + category="governance", + summary=( + "Flags instruction text that appears to encourage bypassing review, " + "CI, PRs, branch protection, or safe integration flow." + ), + limits=( + "Does not audit real GitHub settings, CI configuration, or branch " + "protection." + ), + ), + RuleExplanation( + rule_id="AIRK-GOV004", + title="Unsafe command execution guidance", + category="governance", + summary=( + "Flags instruction text that appears to ask assistants to run " + "destructive, privileged, broad, or externally fetched commands " + "without an explicit confirmation boundary." + ), + limits=( + "Is not a full shell safety analyzer and does not evaluate every " + "possible command form." + ), + ), + RuleExplanation( + rule_id="AIRK-GOV005", + title="Runtime network or LLM dependency guidance", + category="governance", + summary=( + "Flags instruction text that appears to require remote services, LLMs, " + "API tokens, or network behavior that conflicts with local-first " + "boundaries." + ), + limits=( + "Does not inspect real runtime behavior or external service " + "configuration." + ), + ), + RuleExplanation( + rule_id="AIRK-GOV002", + title="Missing secret-handling boundary", + category="governance", + summary=( + "Flags supported instruction files that may lack explicit guidance " + "for handling secrets, tokens, credentials, or sensitive data." + ), + limits=( + "Is not complete secret scanning and does not prove that a repository " + "is free of secrets." + ), + ), + RuleExplanation( + rule_id="AIRK-GOV001", + title="Missing instruction scope or authority", + category="governance", + summary=( + "Flags supported instruction files that may lack clear scope, " + "authority, or precedence boundaries." + ), + limits=( + "Does not resolve organizational policy or prove that instructions " + "are complete." + ), + ), +) + +_RULE_INDEX = {explanation.rule_id: explanation for explanation in RULE_EXPLANATIONS} + + +def list_rule_explanations() -> tuple[RuleExplanation, ...]: + """Return known rule explanations in stable order.""" + return RULE_EXPLANATIONS + + +def get_rule_explanation(rule_id: str) -> RuleExplanation | None: + """Return a known rule explanation by ID.""" + return _RULE_INDEX.get(rule_id.strip().upper()) + + +__all__ = [ + "RULE_EXPLANATIONS", + "RuleExplanation", + "get_rule_explanation", + "list_rule_explanations", +] diff --git a/tests/test_cli.py b/tests/test_cli.py index 2c28f75..523745a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -194,6 +194,81 @@ def test_budget_returns_two_for_invalid_repository_root(self) -> None: self.assertEqual(exit_code, 2) self.assertIn("ERROR: repository root does not exist:", output.getvalue()) + def test_explain_lists_known_rules(self) -> None: + output = io.StringIO() + + with redirect_stdout(output): + exit_code = main(["explain", "--list"]) + + text = output.getvalue() + + self.assertEqual(exit_code, 0) + self.assertIn("agent-rules-kit explain", text) + self.assertIn("Known rules:", text) + self.assertIn("- AIRK-SYS001 [system] Unreadable instruction file", text) + self.assertIn( + "- AIRK-GOV003 [governance] Review or CI bypass guidance", + text, + ) + self.assertIn( + "- AIRK-GOV001 [governance] Missing instruction scope or authority", + text, + ) + + def test_explain_reports_known_rule(self) -> None: + output = io.StringIO() + + with redirect_stdout(output): + exit_code = main(["explain", "AIRK-GOV003"]) + + text = output.getvalue() + + self.assertEqual(exit_code, 0) + self.assertIn("agent-rules-kit explain: AIRK-GOV003", text) + self.assertIn("Title: Review or CI bypass guidance", text) + self.assertIn("Category: governance", text) + self.assertIn("bypassing review", text) + self.assertIn("Does not audit real GitHub settings", text) + + def test_explain_normalizes_known_rule_id(self) -> None: + output = io.StringIO() + + with redirect_stdout(output): + exit_code = main(["explain", "airk-gov003"]) + + self.assertEqual(exit_code, 0) + self.assertIn("agent-rules-kit explain: AIRK-GOV003", output.getvalue()) + + def test_explain_returns_two_for_unknown_rule_id(self) -> None: + output = io.StringIO() + + with redirect_stderr(output): + exit_code = main(["explain", "AIRK-GOV999"]) + + self.assertEqual(exit_code, 2) + self.assertEqual(output.getvalue(), "ERROR: unknown rule ID: AIRK-GOV999\n") + + def test_explain_returns_two_without_rule_id_or_list(self) -> None: + output = io.StringIO() + + with redirect_stderr(output): + exit_code = main(["explain"]) + + self.assertEqual(exit_code, 2) + self.assertEqual(output.getvalue(), "ERROR: explain requires --list or RULE_ID.\n") + + def test_explain_returns_two_for_conflicting_inputs(self) -> None: + output = io.StringIO() + + with redirect_stderr(output): + exit_code = main(["explain", "AIRK-GOV003", "--list"]) + + self.assertEqual(exit_code, 2) + self.assertEqual( + output.getvalue(), + "ERROR: explain accepts either --list or a rule ID.\n", + ) + def test_check_returns_two_for_invalid_repository_root(self) -> None: output = io.StringIO() diff --git a/tests/test_golden_outputs.py b/tests/test_golden_outputs.py index de7f342..4482b14 100644 --- a/tests/test_golden_outputs.py +++ b/tests/test_golden_outputs.py @@ -205,6 +205,23 @@ def test_budget_single_agent_fixture_matches_golden_output(self) -> None: "Next step: review large instruction files before adding more agent guidance.\n", ) + def test_explain_known_rule_matches_golden_output(self) -> None: + result = run_cli(["explain", "AIRK-GOV003"]) + + self.assertEqual(result.exit_code, 0) + self.assertEqual(result.stderr, "") + self.assertEqual( + result.stdout, + "agent-rules-kit explain: AIRK-GOV003\n" + "Title: Review or CI bypass guidance\n" + "Category: governance\n" + "Summary: Flags instruction text that appears to encourage " + "bypassing review, CI, PRs, branch protection, or safe integration " + "flow.\n" + "Limits: Does not audit real GitHub settings, CI configuration, " + "or branch protection.\n", + ) + def test_init_without_mode_matches_golden_error_output(self) -> None: repository = FIXTURE_ROOT / "single-agent" @@ -310,6 +327,30 @@ def test_current_cli_contract_matrix_matches_expected_channels_and_exit_codes(se "stdout_contains": ["Status: no_instruction_files", "Total bytes: 0"], "stderr": "", }, + { + "name": "explain-list", + "args": ["explain", "--list"], + "exit_code": 0, + "stdout_contains": ["Known rules:", "AIRK-GOV001"], + "stderr": "", + }, + { + "name": "explain-known-rule", + "args": ["explain", "AIRK-GOV003"], + "exit_code": 0, + "stdout_contains": [ + "agent-rules-kit explain: AIRK-GOV003", + "Review or CI bypass guidance", + ], + "stderr": "", + }, + { + "name": "explain-unknown-rule", + "args": ["explain", "AIRK-GOV999"], + "exit_code": 2, + "stdout": "", + "stderr": "ERROR: unknown rule ID: AIRK-GOV999\n", + }, { "name": "init-dry-run", "args": ["init", str(FIXTURE_ROOT / "single-agent"), "--dry-run"],