diff --git a/.ai-context/COMMANDS.md b/.ai-context/COMMANDS.md index 2b85392f..30635f31 100644 --- a/.ai-context/COMMANDS.md +++ b/.ai-context/COMMANDS.md @@ -52,16 +52,18 @@ the v1.x compatibility window; new automation should use `--debug-wrapper`. using a stable hash of the canonical project root to isolate same-named checkouts; `--output` explicitly selects another destination, including project directories. `--print` and `--list-files` do not write a bundle. -- `basectl trust status [project]` - inspect one project's manifest command - trust or all discovered command-bearing projects. Its terminal workspace - view is a compact, blocked-first summary; use - `basectl trust status ` for identity digests and review/allow - guidance. +- `basectl trust status [project]` - read-only inspection of local trust. With + no project, show a compact, blocked-first summary of discovered + command-bearing projects; with a project, show detailed identity and review + guidance. Workspace selection uses `--workspace`, then `workspace.root`, then + the parent of `BASE_HOME`. - Generated onboarding trust commands include the canonical workspace and manifest digest; they retain their target when copied to another directory. -- `basectl trust ` - add or remove local approval for - manifest-declared project commands. Revocation covers every approved contract - revision for that canonical checkout and manifest, preserving other checkouts. +- `basectl trust ` - `allow` records local approval for + the current manifest command contract on this machine; `revoke` removes local + approval for that canonical checkout and manifest, covering every approved + contract revision while preserving other checkouts. `--workspace` resolves + the project using the same default. - `basectl prompt ` - list and render repo-owned Markdown prompts for AI-assisted Base workflows. `product-self-review` prints the periodic product assessment prompt with current Base metadata, and `--output ` diff --git a/CHANGELOG.md b/CHANGELOG.md index ea41dd33..92e94007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ numeric next development line is tracked in `DEVELOPMENT_VERSION`. ### Changed +- Clarify `basectl trust` help for workspace scope and approval actions, and make + workspace status reasons identify stale approval. - Make project-less terminal `basectl trust status` output a compact workspace table with blocked projects first, short reasons, and an aggregate summary. diff --git a/cli/bash/commands/basectl/subcommands/trust.sh b/cli/bash/commands/basectl/subcommands/trust.sh index d9a9c1da..c35fc2dd 100644 --- a/cli/bash/commands/basectl/subcommands/trust.sh +++ b/cli/bash/commands/basectl/subcommands/trust.sh @@ -5,14 +5,24 @@ readonly _base_trust_subcommand_sourced base_trust_subcommand_usage() { cat <<'EOF' +Purpose: + Status is read-only: it shows detailed guidance for one project, or a compact + blocked-first summary of discovered command-bearing projects when no project + is supplied. Allow records local approval for manifest-declared commands; + revoke removes local approval. + See `basectl trust status ` for details on what approval covers. + Usage: basectl trust status [project] [options] basectl trust allow [options] basectl trust revoke [options] Options: - --project Select a project explicitly, including one named help. - --workspace Workspace directory to scan. Defaults to workspace.root, then BASE_HOME's parent. + --project Select a project explicitly instead of the + positional argument. + --workspace Workspace for status discovery or project + resolution. Defaults to workspace.root, then + the parent of BASE_HOME. --format Output format for status. Defaults to text. --manifest-sha256 Expected manifest SHA-256 for allow. @@ -20,9 +30,6 @@ Options: Expected SHA-256 for the declared test-requirements file. -v Enable DEBUG logging for this subcommand. -h, --help Show this help text. - -Inspect trust for one project or all discovered projects, and manage local -approval for manifest-declared project commands. EOF } @@ -36,12 +43,14 @@ Usage: basectl trust status [project] [options] Purpose: - Show manifest command trust for one project, or for all discovered - command-bearing projects when no project is supplied. + Read-only trust inspection. Show detailed status for one project, or a + compact blocked-first summary of discovered command-bearing projects when no + project is supplied. Options: - --project Select a project explicitly, including one named help. - --workspace Workspace directory to scan. + --project Select a project explicitly instead of using [project]. + --workspace Workspace to scan for command-bearing projects. + Defaults to workspace.root, then the parent of BASE_HOME. --format Output format. Defaults to text. -v Enable DEBUG logging for this subcommand. @@ -54,12 +63,14 @@ Usage: basectl trust allow [options] Purpose: - Approve the current manifest command contract for one project on this - machine. + Record local approval for the current manifest command contract on this + machine. See `basectl trust status ` for approval-scope details. Options: - --project Select a project explicitly, including one named help. - --workspace Workspace directory to scan. + --project Select a project explicitly instead of using + . + --workspace Workspace used to resolve the project. Defaults + to workspace.root, then the parent of BASE_HOME. --manifest-sha256 Require the current manifest to match this digest. --test-requirements-sha256 Require the declared test-requirements file to match this digest. @@ -76,10 +87,12 @@ Purpose: Remove local manifest command approval for one project. Options: - --project Select a project explicitly, including one named help. - --workspace Workspace directory to scan. - -v Enable DEBUG logging for this subcommand. - -h, --help Show this help text. + --project Select a project explicitly instead of using + . + --workspace Workspace used to resolve the project. Defaults + to workspace.root, then the parent of BASE_HOME. + -v Enable DEBUG logging for this subcommand. + -h, --help Show this help text. EOF ;; *) diff --git a/cli/bash/commands/basectl/tests/help.bats b/cli/bash/commands/basectl/tests/help.bats index a3c47bac..449d409b 100644 --- a/cli/bash/commands/basectl/tests/help.bats +++ b/cli/bash/commands/basectl/tests/help.bats @@ -227,6 +227,48 @@ PY done } +@test "trust help describes status scope and approval effects" { + local help_text + + run_basectl trust --help + [ "$status" -eq 0 ] + help_text="$(tr '\n' ' ' <<<"$output" | tr -s ' ')" + [[ "$help_text" == *"Purpose:"*"Usage:"* ]] + [[ "$help_text" == *"instead of the positional argument"* ]] + [[ "$help_text" == *"Status is read-only"* ]] + [[ "$help_text" == *"compact blocked-first summary of discovered command-bearing projects"* ]] + [[ "$help_text" == *"Allow records local approval"* ]] + [[ "$help_text" == *"revoke removes local approval"* ]] + [[ "$help_text" == *"workspace.root, then the parent of BASE_HOME"* ]] + [[ "$help_text" != *"all discovered projects"* ]] + + run_basectl trust status --help + [ "$status" -eq 0 ] + help_text="$(tr '\n' ' ' <<<"$output" | tr -s ' ')" + [[ "$help_text" == *"Usage:"*"Purpose:"* ]] + [[ "$help_text" == *"Read-only trust inspection"* ]] + [[ "$help_text" == *"command-bearing projects"* ]] + [[ "$help_text" == *"Select a project explicitly instead of using [project]"* ]] + [[ "$help_text" == *"Workspace to scan"* ]] + [[ "$help_text" == *"workspace.root, then the parent of BASE_HOME"* ]] + + run_basectl trust allow demo --help + [ "$status" -eq 0 ] + help_text="$(tr '\n' ' ' <<<"$output" | tr -s ' ')" + [[ "$help_text" == *"Usage:"*"Purpose:"* ]] + [[ "$help_text" == *"Record local approval for the current manifest command contract"* ]] + [[ "$help_text" == *"Workspace used to resolve the project"* ]] + [[ "$help_text" != *scan* ]] + + run_basectl trust revoke demo --help + [ "$status" -eq 0 ] + help_text="$(tr '\n' ' ' <<<"$output" | tr -s ' ')" + [[ "$help_text" == *"Usage:"*"Purpose:"* ]] + [[ "$help_text" == *"Remove local manifest command approval"* ]] + [[ "$help_text" == *"Workspace used to resolve the project"* ]] + [[ "$help_text" != *scan* ]] +} + @test "command reference documents nested help and repo check release parity" { local command_reference="$BASE_REPO_ROOT/docs/command-reference.md" local repo_check_row @@ -301,14 +343,21 @@ PY @test "AI command context includes current clone and update surfaces" { local commands_file="$BASE_REPO_ROOT/.ai-context/COMMANDS.md" + local commands_text + + commands_text="$(tr '\n' ' ' <"$commands_file" | tr -s ' ')" grep -Fqx -- "- \`basectl workspace \` -" "$commands_file" grep -Fqx -- " - \`workspace clone\` mutates repository checkouts only when invoked directly;" "$commands_file" grep -Fqx -- "- \`basectl repo \` -" "$commands_file" grep -Fqx -- "- \`basectl update [project]\` - update Base or a named project using the" "$commands_file" grep -Fqx -- "- \`basectl docs\` - open the Base documentation home page on GitHub." "$commands_file" - grep -Fqx -- "- \`basectl trust status [project]\` - inspect one project's manifest command" "$commands_file" - grep -Fqx -- "- \`basectl trust \` - add or remove local approval for" "$commands_file" + [[ "$commands_text" == *'- `basectl trust status [project]` - read-only inspection of local trust.'* ]] + [[ "$commands_text" == *'compact, blocked-first summary of discovered command-bearing projects'* ]] + [[ "$commands_text" == *'Workspace selection uses `--workspace`, then `workspace.root`, then the parent of `BASE_HOME`.'* ]] + [[ "$commands_text" == *'- `basectl trust ` - `allow` records local approval for the current manifest command contract on this machine; `revoke` removes local approval'* ]] + [[ "$commands_text" == *'covering every approved contract revision while preserving other checkouts.'* ]] + [[ "$commands_text" == *'`--workspace` resolves the project using the same default.'* ]] } @test "public command inventories include demo, run, and release" { @@ -405,12 +454,22 @@ PY grep -Fqx -- "| \`basectl docs\` | Open the Base documentation home page on GitHub. | \`--show-url\` |" "$command_reference" } +@test "manifest trust docs define workspace stale-approval labels" { + local trust_doc="$BASE_REPO_ROOT/docs/manifest-command-trust.md" + local trust_text + + trust_text="$(tr '\n' ' ' <"$trust_doc" | tr -s ' ')" + [[ "$trust_text" == *'`Approval stale: manifest`'* ]] + [[ "$trust_text" == *'`Approval stale: test reqs`'* ]] + [[ "$trust_text" == *'contract changed since approval'* ]] +} + @test "command reference documents trust commands" { local command_reference="$BASE_REPO_ROOT/docs/command-reference.md" - grep -Fqx -- "| \`basectl trust status [project]\` | Show detailed trust guidance for one project, or a compact blocked-first table for discovered command-bearing projects. | \`--project \`, \`--workspace \`, \`--format \` |" "$command_reference" - grep -Fqx -- "| \`basectl trust allow \` | Approve the current manifest command contract on this machine. | \`--project \`, \`--workspace \`, \`--manifest-sha256 \`, \`--test-requirements-sha256 \` |" "$command_reference" - grep -Fqx -- "| \`basectl trust revoke \` | Remove local manifest command approval. | \`--project \`, \`--workspace \` |" "$command_reference" + grep -Fqx -- "| \`basectl trust status [project]\` | Read-only inspection: show detailed trust guidance for one project, or a compact blocked-first summary for discovered command-bearing projects. | \`--project \`, \`--workspace \`, \`--format \` |" "$command_reference" + grep -Fqx -- "| \`basectl trust allow \` | Record local approval for the project's current manifest command contract on this machine. | \`--project \`, \`--workspace \`, \`--manifest-sha256 \`, \`--test-requirements-sha256 \` |" "$command_reference" + grep -Fqx -- "| \`basectl trust revoke \` | Remove local manifest command approval for one project. | \`--project \`, \`--workspace \` |" "$command_reference" } @test "command reference documents repo and Project configuration options" { diff --git a/cli/bash/commands/basectl/tests/trust.bats b/cli/bash/commands/basectl/tests/trust.bats index 0b4b0d9b..a293309d 100644 --- a/cli/bash/commands/basectl/tests/trust.bats +++ b/cli/bash/commands/basectl/tests/trust.bats @@ -31,7 +31,7 @@ load ./basectl_helpers.bash [[ "$output" == *"--manifest-sha256 "* ]] [[ "$output" == *"--test-requirements-sha256 "* ]] [[ "$output" != *"--format"* ]] - [[ "$output" != *"basectl trust status"* ]] + [[ "$output" == *"basectl trust status "* ]] run_basectl trust revoke --help diff --git a/cli/python/base_trust/engine.py b/cli/python/base_trust/engine.py index b4a29404..1c962fee 100644 --- a/cli/python/base_trust/engine.py +++ b/cli/python/base_trust/engine.py @@ -49,6 +49,15 @@ help="Manage local approval for manifest-declared project commands.", ) +_STATUS_WORKSPACE_HELP = ( + "Workspace directory to scan for command-bearing projects. " + "Defaults to workspace.root, then the parent of BASE_HOME." +) +_PROJECT_WORKSPACE_HELP = ( + "Workspace directory used to resolve the project. " + "Defaults to workspace.root, then the parent of BASE_HOME." +) + def main(argv: list[str] | None = None) -> int: return base_cli.run_app(app, argv) @@ -58,7 +67,7 @@ def main(argv: list[str] | None = None) -> int: @base_cli.argument("project", required=False) @base_cli.option( "--workspace", - help="Workspace directory to scan. Defaults to workspace.root, then BASE_HOME's parent.", + help=_STATUS_WORKSPACE_HELP, ) @base_cli.option( "--format", @@ -104,7 +113,7 @@ def status_command(ctx: base_cli.Context, project: str | None, workspace: str | @base_cli.argument("project") @base_cli.option( "--workspace", - help="Workspace directory to scan. Defaults to workspace.root, then BASE_HOME's parent.", + help=_PROJECT_WORKSPACE_HELP, ) @base_cli.option("--manifest", "manifest_path", help="Resolved base_manifest.yaml path to verify.") def require_command(ctx: base_cli.Context, project: str, workspace: str | None, manifest_path: str | None) -> int: @@ -130,7 +139,7 @@ def require_command(ctx: base_cli.Context, project: str, workspace: str | None, @base_cli.argument("project") @base_cli.option( "--workspace", - help="Workspace directory to scan. Defaults to workspace.root, then BASE_HOME's parent.", + help=_PROJECT_WORKSPACE_HELP, ) @base_cli.option("--manifest-sha256", help="Expected SHA-256 digest of base_manifest.yaml.") @base_cli.option( @@ -177,7 +186,7 @@ def allow_command( @base_cli.argument("project") @base_cli.option( "--workspace", - help="Workspace directory to scan. Defaults to workspace.root, then BASE_HOME's parent.", + help=_PROJECT_WORKSPACE_HELP, ) def revoke_command(ctx: base_cli.Context, project: str, workspace: str | None) -> int: try: @@ -289,8 +298,8 @@ def _workspace_status_reason_label(reason: str) -> str: labels = { "allowed": "Approved", "not_allowed": "Not approved", - "manifest_changed": "Manifest changed", - "test_requirements_changed": "Test requirements changed", + "manifest_changed": "Approval stale: manifest", + "test_requirements_changed": "Approval stale: test reqs", } if reason in labels: return labels[reason] diff --git a/cli/python/base_trust/tests/test_command_error_paths.py b/cli/python/base_trust/tests/test_command_error_paths.py index ce139860..56b7b6fb 100644 --- a/cli/python/base_trust/tests/test_command_error_paths.py +++ b/cli/python/base_trust/tests/test_command_error_paths.py @@ -153,8 +153,8 @@ def test_workspace_status_text_summarizes_blocked_reasons_before_allowed_project ) assert result.exit_code == 0, result.output - assert "changed-manifest Blocked Manifest changed" in result.stdout - assert "changed-requirements Blocked Test requirements changed" in result.stdout + assert "changed-manifest Blocked Approval stale: manifest" in result.stdout + assert "changed-requirements Blocked Approval stale: test reqs" in result.stdout assert "fresh Blocked Not approved" in result.stdout assert "allowed Allowed Approved" in result.stdout assert result.stdout.index("changed-manifest Blocked") < result.stdout.index("allowed Allowed") @@ -229,6 +229,81 @@ def display_width(value: str) -> int: assert len({display_width(line.split("Blocked", maxsplit=1)[0]) for line in rows}) == 1 +def test_workspace_status_reason_labels_survive_narrow_terminal_fitting( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + _force_terminal_output(monkeypatch) + monkeypatch.setattr(base_cli_output, "_terminal_width", lambda _stream: 60) + # At width 60, the 24-character project, 7-character status, and 25-character + # reason use all columns with two 2-space gaps; the long name forces truncation + # while the longest reason label fits with zero slack. + long_project_name = "project-" + "x" * 32 + assert len(long_project_name) == 40 + statuses = ( + engine.TrustStatus( + status="blocked", + reason="manifest_changed", + identity=engine.ManifestCommandTrustIdentity( + project_name=long_project_name, + project_root=tmp_path, + manifest_path=tmp_path / "base_manifest.yaml", + manifest_sha256="a" * 64, + identity_key=long_project_name, + ), + ), + engine.TrustStatus( + status="blocked", + reason="test_requirements_changed", + identity=engine.ManifestCommandTrustIdentity( + project_name="demo", + project_root=tmp_path, + manifest_path=tmp_path / "base_manifest.yaml", + manifest_sha256="b" * 64, + identity_key="demo", + ), + ), + ) + + engine._print_workspace_status_text(tmp_path, statuses) # pylint: disable=protected-access + + output = capsys.readouterr().out + rows = [line for line in output.splitlines() if "Blocked" in line] + assert len(rows) == 2 + assert any("Approval stale: manifest" in line for line in rows) + assert any("Approval stale: test reqs" in line for line in rows) + + +@pytest.mark.parametrize( + ("command", "workspace_help", "expect_scan"), + [ + ("status", "Workspace directory to scan for command-bearing projects.", True), + ("require", "Workspace directory used to resolve the project.", False), + ("allow", "Workspace directory used to resolve the project.", False), + ("revoke", "Workspace directory used to resolve the project.", False), + ], +) +def test_trust_python_help_describes_workspace_resolution( + tmp_path: Path, + command: str, + workspace_help: str, + expect_scan: bool, +) -> None: + result = invoke( + engine.app, + [command, "--help"], + home=tmp_path / "home", + env={"BASE_HOME": str(tmp_path / "base")}, + ) + + assert result.exit_code == 0, result.output + help_text = " ".join(result.stdout.split()) + assert workspace_help in help_text + assert "Defaults to workspace.root, then the parent of BASE_HOME." in help_text + assert ("scan" in help_text) is expect_scan + + def test_workspace_status_reason_fallback_preserves_acronyms() -> None: assert engine._workspace_status_reason_label("remote_HTTP_status") == "Remote HTTP Status" # pylint: disable=protected-access diff --git a/docs/command-reference.md b/docs/command-reference.md index 799b9ab5..31964fc1 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -146,9 +146,9 @@ is requested on macOS, Base warns if `osascript` is not available. | `basectl demo [project]` | Run a project-owned demo script. | `--project `, `--workspace `, `--dry-run`, `-- ` | | `basectl devcontainer [project]` | Preview or write `.devcontainer/devcontainer.json` from a Base manifest. Dry-run is the default. | `--project `, `--workspace `, `--format `, `--write` | | `basectl devenv-report [project]` | Classify Base manifest fields for Nix/devenv planning without generating files or requiring Nix. | `--project `, `--workspace `, `--format ` | -| `basectl trust status [project]` | Show detailed trust guidance for one project, or a compact blocked-first table for discovered command-bearing projects. | `--project `, `--workspace `, `--format ` | -| `basectl trust allow ` | Approve the current manifest command contract on this machine. | `--project `, `--workspace `, `--manifest-sha256 `, `--test-requirements-sha256 ` | -| `basectl trust revoke ` | Remove local manifest command approval. | `--project `, `--workspace ` | +| `basectl trust status [project]` | Read-only inspection: show detailed trust guidance for one project, or a compact blocked-first summary for discovered command-bearing projects. | `--project `, `--workspace `, `--format ` | +| `basectl trust allow ` | Record local approval for the project's current manifest command contract on this machine. | `--project `, `--workspace `, `--manifest-sha256 `, `--test-requirements-sha256 ` | +| `basectl trust revoke ` | Remove local manifest command approval for one project. | `--project `, `--workspace ` | Manifest-declared `test`, `run`, `build`, `demo`, and activation surfaces are project-owned code executed from the project root. Review manifests from diff --git a/docs/manifest-command-trust.md b/docs/manifest-command-trust.md index 29ef9917..f89ab10c 100644 --- a/docs/manifest-command-trust.md +++ b/docs/manifest-command-trust.md @@ -175,7 +175,10 @@ compact workspace table with blocked projects first, a short reason for each status, and aggregate allowed/blocked counts. It states the trust-scope limit once and points to `basectl trust status ` for the detailed identity, review, and allow guidance. An explicit project keeps the detailed text report; -JSON/YAML/CSV/TSV workspace output keeps its existing schema and columns. +JSON/YAML/CSV/TSV workspace output keeps its existing schema and columns. The +summary labels `Approval stale: manifest` and `Approval stale: test reqs` mean +the corresponding manifest-command or declared test-requirements contract +changed since approval. For schema-v1 compatibility, explicit single-project JSON continues to report the underlying allow-record state, including `status: blocked` and an