Skip to content

chore: typography cleanup + polish#46

Merged
leodido merged 1 commit into
mainfrom
chore/cleanup-emdash-internal-refs
May 3, 2026
Merged

chore: typography cleanup + polish#46
leodido merged 1 commit into
mainfrom
chore/cleanup-emdash-internal-refs

Conversation

@leodido
Copy link
Copy Markdown
Owner

@leodido leodido commented May 3, 2026

Stacked on #45. Top of the cli-track stack now (#38#39#40#41#42#43#45 → this). The base will retarget downward as each parent merges.

Two concerns rolled into one PR because they share the same files and the polish changes were caught while reviewing the typography sweep.


Part 1: typography cleanup

Em-dashes (, U+2014) used liberally across docs and a couple of source files. They are an LLM tell, and they don't match the project's plain-ASCII voice (the rest of the repo uses colons, parens, and sentence splits). The user-visible CLI output FAIL: feature — reason from kfeatures check also carried one.

Plus: one leftover (PR 1) parenthetical in a comment in cmd/kfeatures/main.go that referred to an internal stack position. References to internal-only files (spec.md, spec-bpffs-mount.md, where-the-real-value-of-structcli-v017-is.md) were already absent from tracked content; verified by scan.

Scope

All tracked files were scanned, not just *.md:

em-dash hits before: 52 (across 11 files)
em-dash hits after:  0
internal-ref hits before: 1
internal-ref hits after:  0

En-dashes inside numeric ranges (1019, 2029, 19 in the exit-code documentation) are kept. They are typographically correct (Keep-a-Changelog style) and out of scope per F1's literal definition (em-dash, U+2014, only).

Approach

Case-by-case:

  • term — definition list items → term: definition (colon).
  • Mid-sentence em-dashes → split into two sentences, swap to a colon, or wrap in parens depending on what reads better.
  • Code-comment em-dashes → semicolon or colon.
  • Documented user-visible format strings → spaced hyphen, in lockstep with the binary's actual output.

User-visible format change (in lockstep)

cmd/kfeatures/main.go previously printed FAIL: %s — %s\n on FeatureError. It now prints FAIL: %s - %s\n. The contract is documented in two places that change together:

  • CHANGELOG.md [Unreleased] (line that documents the --mcp / WithFlagErrors story for the kfeatures check business-outcome contract).
  • CONTRIBUTING.md ## CLI conventions → "Two error contracts" subsection.

The bats suite (cli_common, cli_linux, cli_mcp) does not assert on the em-dash glyph, so behaviour stays green.

The one renamed test (mcp: stdout is JSON-RPC only — no leakage from command handlersmcp: stdout is JSON-RPC only, no leakage from command handlers) passes unchanged.


Part 2: polish

After the typography sweep landed, a self-review of the cli-track stack surfaced four small defects that fit naturally alongside the typography pass (same files, same review batch, no extra reviewer churn).

Issue: mcp_call 2>/dev/null masked failures (test/cli_mcp.bats)

The MCP test harness redirected stderr to /dev/null, so any panic, structcli envelope error emitted before the MCP loop, or in-MCP diagnostic was invisible at test-failure time. A regression would manifest as no response with id N from the response-extractor instead of the actual error.

Demonstrated by feeding malformed input to /tmp/kfeatures --mcp:

{"error":"error","exit_code":1,"message":"invalid character 'o' in literal null (expecting 'u')","command":"kfeatures"}

…which the old harness silently dropped.

Fix: mcp_call now captures stderr to a temp file and surfaces it on non-zero exit:

kfeatures --mcp exited 1; stderr: {"error":"error","exit_code":1,...}

Issue: fragile JSON-escape assertion (test/cli_common.bats)

The check: legacy alias is rejected test asserted on a JSON-escaped substring of structcli's prose message field:

assert_output --partial 'unknown feature: \"bpffs\"'

That ties the test to structcli's specific escaping convention. Any future structcli rewording (or change to escape policy) breaks it even when behaviour is correct.

Fix: assert on the structured error / flag / got fields, which are the documented contract of StructuredError:

assert d['error'] == 'invalid_flag_value'
assert d['got'] == 'bpffs'
assert d['flag'] == 'require'

Issue: inMCPMode coupling and defer-safety undocumented (cmd/kfeatures/main.go)

inMCPMode(c) returns true iff c.OutOrStdout() != os.Stdout. This works because the only code path that calls cmd.SetOut in structcli v0.17.0 is the MCP wrapper (mcp.go lines 399 and 417). Verified by grepping the structcli source for non-test callers: zero hits outside mcp.go.

The proxy is latent-fragile: any future structcli capability that calls SetOut for another reason would silently flip inMCPMode to true outside MCP, causing the check/config os.Exit(1) carve-out to be skipped and the typed errors to be printed as structured-error envelopes (collapsing the documented FAIL: feature - reason and {ok,feature,reason} contracts).

Separately: os.Exit(1) inside a RunE skips deferred functions. There are no defers in those RunEs today; adding any in a future PR (file close, lock release, telemetry flush) would silently leak.

Fix: comment block on inMCPMode now spells out:

  1. The structcli-version-pinned coupling assumption and what to do if it breaks.
  2. The no-defer-in-os.Exit-RunE invariant and why the comment exists.

No code change. The comment is the deliverable: it preserves the invariants for future readers (human or agent).

Issue: misleading mcpServerVersion comment (cmd/kfeatures/main.go)

The doc comment claimed mcpServerVersion() "mirrors what kfeatures version prints". That is wrong:

  • kfeatures version prints kfeatures <ver> (<commit>) built <date> (or kfeatures (dev) as fallback).
  • mcpServerVersion() returns just the bare version string (or "dev"), which goes into MCP's serverInfo.version field; the name field already carries "kfeatures".

Both are correct for their respective protocols (MCP separates name/version; the CLI is a single human-readable line). The comment was the wrong part.

Fix: comment now accurately describes the function's purpose and notes why MCP and CLI render differently.


Diff shape

 AGENTS.md              | 34 +++++++++++------------
 CHANGELOG.md           | 18 ++++++-------
 CONTRIBUTING.md        | 32 +++++++++++-----------
 README.md              | 26 +++++++++---------
 cmd/kfeatures/main.go  | 73 +++++++++++++++++++++++++++++++-------------------
 doc.go                 |  2 +-
 requirements.go        |  2 +-
 test/cli_common.bats   | 14 +++++++---
 test/cli_linux.bats    |  2 +-
 test/cli_mcp.bats      | 16 +++++++++--
 test/cli_nonlinux.bats |  2 +-
 11 files changed, 129 insertions(+), 92 deletions(-)

Tested

  • go build ./...
  • go vet ./...
  • go test -count=1 ./... ✓ (forced fresh, both packages green)
  • bats test/cli_common.bats test/cli_linux.bats test/cli_mcp.bats ✓ (34/34, including the renamed mcp test)
  • mcp_call stderr-surfacing demonstrated against malformed-input invocation

Self-review notes (intentionally NOT addressed in this PR)

The self-review also surfaced items that I deliberately did not act on, so reviewers know they were considered:

  • Excludes list hardcodes shell names. Cobra ships exactly four shells (bash/zsh/fish/powershell). A fifth would be a cobra-version-bump event surfaced immediately on tools/list. Theoretical, not a defect.
  • ProbeWith caching interaction with MCP server mode. Initially flagged. Re-verification: ProbeWith does not cache (only Probe() does), so each MCP tools/call gets a fresh probe. Non-issue.
  • README exit-code table omits codes 13/15/20-26. Initially flagged. Re-verification: those codes (ValidationFailed, InvalidFlagEnum, Config*, Env*) are not reachable from kfeatures' surface (no struct validators, no config files, no env-only flags). Table is accurate for kfeatures.
  • Historical CHANGELOG entries had em-dashes too. Discussed and consciously chosen: pre-1.0 project, no known external links to changelog anchors, fixing now is cheaper than fixing after first inbound link.

Out of scope (won't be in this PR even as a follow-up)

  • spec.md, spec-bpffs-mount.md, where-the-real-value-of-structcli-v017-is.md are working notes on disk but not tracked in git. They remain local-only.
  • Any non-U+2014 unicode characters (en-dashes in numeric ranges, the existing arrows in AGENTS.md doc-split section, the in PR-stack notation in PR bodies) are intentional and untouched.

@leodido leodido self-assigned this May 3, 2026
@leodido leodido force-pushed the chore/cleanup-emdash-internal-refs branch 3 times, most recently from aa295df to d9e3c60 Compare May 3, 2026 22:01
@leodido leodido changed the title chore: drop em-dashes and stray internal refs from tracked files chore: typography cleanup and post-self-review polish May 3, 2026
@leodido leodido changed the title chore: typography cleanup and post-self-review polish chore: typography cleanup + polish May 3, 2026
@leodido leodido force-pushed the chore/devcontainer-noreply-identity branch from 7460e41 to 642f014 Compare May 3, 2026 22:47
@leodido leodido changed the base branch from chore/devcontainer-noreply-identity to main May 3, 2026 22:48
Two concerns rolled into one PR because they share the same files and
the polish changes were caught while reviewing the typography sweep.

## Typography cleanup

Em-dashes (U+2014) are an LLM tell and clash with the project's
plain-ASCII voice. Rephrase case-by-case across all tracked files:
colon, parens, semicolon, or sentence split per what reads better.
En-dashes inside numeric ranges (10-19, 20-29, 1-9) are kept; they are
typographically correct and out of scope.

The 'PR 1' parenthetical in cmd/kfeatures/main.go (a leftover stack
position reference) is dropped: internal stack-position refs do not
belong in landed code.

User-visible format change in lockstep:
- cmd/kfeatures/main.go's 'kfeatures check' FeatureError path now
  prints 'FAIL: feature - reason' (was 'FAIL: feature -- reason' with
  em-dash). The contract is mirrored in CHANGELOG.md '[Unreleased]'
  and CONTRIBUTING.md 'Two error contracts' so docs stay accurate.
- README.md usage examples that print the same kind of message switch
  to ASCII hyphen.

## Post-self-review polish

Self-review of the cli-track stack surfaced four small defects that
fit naturally alongside the typography pass:

- test/cli_mcp.bats: mcp_call no longer redirects stderr to /dev/null.
  It captures stderr and surfaces it on non-zero exit so future
  panics, structcli envelope errors emitted before the MCP loop, or
  in-MCP diagnostics are visible at test-failure time instead of
  manifesting as a confusing 'no response with id N' assertion.
- test/cli_common.bats: 'check: legacy alias is rejected' asserted
  on a JSON-escaped substring of structcli's prose 'message' field.
  Switched to assert on the structured 'got' / 'flag' / 'error'
  fields, which are the documented contract.
- cmd/kfeatures/main.go inMCPMode: comment now spells out the
  coupling to structcli's MCP wrapper being the sole SetOut caller
  (true at v0.17.0; latent risk if that changes) and the
  no-defer-in-os.Exit-RunE invariant (true today; comment exists to
  keep it true).
- cmd/kfeatures/main.go mcpServerVersion: comment previously claimed
  it 'mirrors what kfeatures version prints', which is wrong (the
  CLI version subcommand emits a richer 'kfeatures <ver> (<commit>)
  built <date>' line; MCP serverInfo carries just the version
  string). Comment now says what it actually does and why the two
  paths differ.

## Tested

- go build / go vet / go test -count=1 ./... green
- bats test/cli_common.bats test/cli_linux.bats test/cli_mcp.bats:
  34/34 green, including the renamed 'mcp: stdout is JSON-RPC only,
  no leakage from command handlers' test
- mcp_call stderr-surfacing verified by feeding malformed input to
  /tmp/kfeatures --mcp: now reports 'kfeatures --mcp exited 1;
  stderr: {error envelope}' instead of swallowing it

Co-authored-by: Ona <no-reply@ona.com>
@leodido leodido force-pushed the chore/cleanup-emdash-internal-refs branch from d9e3c60 to 01cea4c Compare May 3, 2026 22:49
@leodido leodido merged commit 447b6a3 into main May 3, 2026
6 checks passed
@leodido leodido deleted the chore/cleanup-emdash-internal-refs branch May 3, 2026 22:51
@leodido leodido added the chore label May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant