Skip to content

B1.scaffold: Go module + cobra + version + snapshot infrastructure - #116

Merged
thrillmot merged 2 commits into
v1-go-rewritefrom
feat/go-b1-scaffold
Jun 2, 2026
Merged

B1.scaffold: Go module + cobra + version + snapshot infrastructure#116
thrillmot merged 2 commits into
v1-go-rewritefrom
feat/go-b1-scaffold

Conversation

@thrillmot

Copy link
Copy Markdown
Collaborator

Summary

Wave B1.scaffold of the Python → Go rewrite. Lands the bare-minimum
foundation that every subsequent wave builds on:

  • go.mod (module github.com/thrillmade/logmind, Go 1.22+) +
    go.sum.
  • Package layout: cmd/logmind/main.go (thin entry shim) →
    internal/cli/ (cobra root + subcommand registration) +
    internal/version/ (version constants).
  • logmind --version AND logmind version both emit the
    protocol-contract line:
    logmind 1.0.0-dev (spec 0.1.0-draft)
    
  • Snapshot test infrastructure under internal/cli/testdata/ with a
    shared -update flag (driven by make snapshot). Five tests cover
    the format from three angles: in-process function call, in-process
    cobra tree exec, and built-binary subprocess.
  • Makefile: make build, make test, make snapshot,
    make verify-parity (placeholder), make tidy, make clean.
  • .github/workflows/go-test.yml: matrix CI (ubuntu-latest/macos-latest,
    Go 1.22 + 1.24) gating only the v1-go-rewrite branch + its feature
    branches. Existing test.yml (pytest matrix) continues to gate main
    untouched.
  • README touch under "Installation" noting the rewrite is in progress
    on v1-go-rewrite; PyPI keeps shipping from main.

Out of scope (intentionally)

  • No subcommand ports beyond --version. Not init, not log, not
    show, not search. Each gets its own wave PR.
  • src/logmind/ (Python) is untouched. Both implementations coexist
    through the rewrite.
  • main branch is untouched. Wave PRs target v1-go-rewrite; the
    final cutover PR v1-go-rewrite → main becomes v1.0.0.
  • No pkg/ directory. Everything is internal/ until SPEC.md locks
    a public Go API.
  • make verify-parity is a stub. The first wave that ports a
    real subcommand fills it in by diffing Go stdout vs Python v0.6.14
    stdout for the same args.

Snapshot test pattern (load-bearing for future waves)

Every future wave that ports a subcommand SHOULD follow this pattern:

  1. Add a golden file: internal/cli/testdata/<command>.golden — raw
    stdout bytes the command should produce.
  2. Add a TestXxx_InProcess test that drives NewRootCmd() with
    SetArgs([...]), captures the buffer, and diffs against the golden.
  3. Add a TestXxx_Subprocess test that builds the real binary (via
    the repoRootFromCaller helper) and execs it for an extra byte-level
    guarantee.
  4. Honour the shared -update flag so make snapshot regenerates
    every golden in one pass.

The --version test in internal/cli/version_test.go is the
canonical example.

Decisions baked in (recorded via logmind log)

  • cobra over urfave/cli/v2 or stdlib flag — ecosystem ubiquity
    (kubectl, gh, hugo, helm) + group/subcommand shape mirrors Python
    click closely.
  • internal/ rather than pkg/ — v1.0's public surface IS the
    binary; no other repo should import logmind's guts as a Go library
    yet, and internal/ enforces that at compile time. Promote
    packages later if SPEC.md locks a public API.
  • Plain-text goldens + byte-identical exec diff — keeps the
    parity gate against Python v0.6.14 a one-line diff (Python stdout
    is the future golden for the matching Go command). No JSON
    wrapping, no cmpopts, just bytes.
  • New --version format, NOT byte-identical to Python's click
    default (logmind, version 0.6.14). v1.0 publishes a stable
    machine-parseable line including the spec version so downstream
    tooling (clud-bug, tokenomics) can detect protocol skew.

Open questions for review

  • Config library: I did NOT pull in viper / koanf / kong. Wave B1
    has no config to read. The first wave that needs config can decide.
    Flagging here so a reviewer can pre-redirect if there's already a
    preferred library on the roadmap.
  • Go floor at 1.22: matches the GitHub-hosted runner default at
    time of writing. Happy to bump to 1.23 if there's a stdlib feature
    the rewrite already wants.
  • site/node_modules/flatted/golang/ ships a third-party Go
    package inside the docs site's npm tree. make test scopes to
    ./cmd/... ./internal/... to skip it cleanly; flagging in case the
    site/node_modules tree should be moved out of the working copy or
    gitignored entirely.

Test plan

  • make build produces bin/logmind.
  • ./bin/logmind --version and ./bin/logmind version both print
    logmind 1.0.0-dev (spec 0.1.0-draft).
  • make test green (5 tests pass in internal/cli).
  • make snapshot is a no-op when goldens already match.
  • CI's go-test job runs on this PR (Ubuntu × Go 1.22, Ubuntu × Go 1.24,
    macOS × Go 1.22) and stays green.
  • Existing test.yml pytest matrix continues to pass (Go changes
    don't touch Python paths).

@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

Deployment failed with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/thrillmot-s-projects?upgradeToPro=build-rate-limit

@claude

claude Bot commented Jun 2, 2026

Copy link
Copy Markdown

Claude finished @thrillmot's task in 5m 48s —— View job


🐛 Clud Bug review

This round: 1 critical · 1 minor · 0 resolved from prior · 0 still open

Found: 1 🔴 / 1 🟡 / 0 🟣

Per-skill scan

  • [critical-issues-only]: scanned all 14 files. 1 critical finding (double error printing), 1 minor (snapshot fragility).
  • [evidence-based-review]: all findings anchored to specific file:line with concrete failure modes.
  • [respect-existing-conventions]: no convention conflicts; new Go layout follows standard Go project conventions and matches existing codebase patterns.

🔴 [critical-issues-only]: Double error printing in cmd/logmind/main.go:20 — cobra's default SilenceErrors: false already calls c.PrintErrln("Error:", ...) to stderr in ExecuteC() before returning; main.go then calls fmt.Fprintln(os.Stderr, err) again. Every error case (unknown command, flag parse failure, RunE return) prints twice.

Reasoning

internal/cli/root.go sets SilenceUsage: true but not SilenceErrors, so cobra's default printing is active. The comment in main.go acknowledges "cobra already prints its own errors" but still re-prints. Fix: add SilenceErrors: true to the root command in root.go so this fmt.Fprintln becomes the single authoritative printer, or remove the fmt.Fprintln and rely on cobra's default.

🟡 [critical-issues-only]: make snapshot passes -update to all test binaries in ./cmd/... ./internal/..., but future test packages that don't register flag.Bool("update", ...) will fail with flag provided but not defined: -update (Makefile:52).

Reasoning

Only internal/cli/version_test.go registers the flag today. The PR body tells future waves to "honour the shared -update flag" but doesn't make the requirement explicit at the Makefile level. Go test binaries exit 2 on unrecognised flags, so any new package's tests will break make snapshot until they declare the flag. A comment in the target noting the requirement, or scoping with a naming convention (e.g. -run TestSnapshot), would prevent the confusing failure.


Skills referenced: [critical-issues-only, evidence-based-review, respect-existing-conventions]

Comment thread cmd/logmind/main.go Outdated
Comment thread Makefile Outdated
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🐛 Clud Bug review

This round: 1 critical · 1 minor · 0 resolved from prior · 0 still open

Found: 1 🔴 / 1 🟡 / 0 🟣

Per-skill scan

  • [critical-issues-only]: scanned all 14 files. 1 critical finding (double error printing in main.go), 1 minor finding (snapshot target fragility).
  • [evidence-based-review]: applied to all findings. all claims anchored to specific file:line with concrete failure modes.
  • [respect-existing-conventions]: no convention conflicts found; new Go code follows standard layout and cobra patterns already in repo.

Critical findings

🔴 [critical-issues-only]: Cobra double-prints errors before returning; main.go then prints again, producing duplicate stderr on every error path (cmd/logmind/main.go:20).

Reasoning

root.go sets SilenceUsage: true but NOT SilenceErrors, so cobra's default ExecuteC() calls c.PrintErrln("Error:", err.Error()) to stderr before returning. main.go then calls fmt.Fprintln(os.Stderr, err) — the comment acknowledges cobra prints its own errors yet still re-prints. Fix: add SilenceErrors: true to the root command in root.go to suppress cobra's built-in printing and let main.go be the single authoritative printer.

Minor findings

🟡 [critical-issues-only]: make snapshot passes -update to all test binaries; future packages without flag.Bool("update",...) registration will fail with flag provided but not defined (Makefile:52).

Reasoning

PKG covers ./cmd/... ./internal/... — any new test package that doesn't declare var update = flag.Bool("update", ...) will cause make snapshot to exit 2 with 'flag provided but not defined: -update'. Currently harmless (only internal/cli has tests and registers the flag), but the PR positions this scaffold as the pattern for future waves. A comment in the target or a naming convention (e.g. -run TestSnapshot) would prevent confusing failures.

Skills referenced: [critical-issues-only, evidence-based-review, respect-existing-conventions]

@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
logmind-site Ready Ready Preview, Comment Jun 2, 2026 4:19am

@claude

claude Bot commented Jun 2, 2026

Copy link
Copy Markdown

Claude finished @thrillmot's task in 2m 5s —— View job


  • Load tools
  • Fetch prior SHA + incremental diff
  • Walk prior review threads (both already resolved by author)
  • Review incremental changes — both fixes verified correct
  • Emit structured output

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🐛 Clud Bug review

This round: 0 critical · 0 minor · 0 resolved from prior · 0 still open

Found: 0 🔴 / 0 🟡 / 0 🟣

Per-skill scan

  • [critical-issues-only]: scanned incremental delta (main.go, Makefile, docs). 0 new findings — both prior issues correctly fixed.
  • [evidence-based-review]: 0 findings this pass — no claims to anchor.
  • [respect-existing-conventions]: 0 findings — fix commits follow established patterns.

Skills referenced: [critical-issues-only, evidence-based-review, respect-existing-conventions]

@thrillmot
thrillmot merged commit 20e2d77 into v1-go-rewrite Jun 2, 2026
17 checks passed
@thrillmot
thrillmot deleted the feat/go-b1-scaffold branch June 2, 2026 04:22
thrillmot added a commit that referenced this pull request Jun 2, 2026
…119)

## Summary

Wave B3 of the logmind Go rewrite — ports the derived-doc generators and
the rebase wrapper.

- 4 new subcommands: `timeline`, `file-structure`, `tree`, `rebase`
- 4 new internal packages: `internal/config`, `internal/decisions`,
`internal/timeline`, `internal/tree`
- 2 new helpers on `internal/gitcli`: `DefaultBranch` (5-step search
mirror), `RunCaptured` (stderr-exposing variant for rebase)
- `Makefile` `SNAPSHOT_PKGS` extended with `internal/timeline/...` +
`internal/tree/...`

Built on top of B1 + B2 (PRs #116 + #117). Targets `v1-go-rewrite`.

## Byte-identical output vs Python v0.6.14

Verified against the actual logmind repo (112 decisions across 6 months,
~120KB unbounded tree):

| Command | Result |
|---|---|
| `logmind timeline` (brief, default) | BYTE-IDENTICAL (1647 bytes) |
| `logmind timeline --full` | BYTE-IDENTICAL (27407 bytes) |
| `logmind file-structure` (depth 2) | BYTE-IDENTICAL (4180 bytes) |
| `logmind file-structure --max-depth 0` (unbounded) | BYTE-IDENTICAL
(121729 bytes) |
| `logmind file-structure --max-depth 1` | BYTE-IDENTICAL (975 bytes) |
| `logmind file-structure --max-depth 3` | BYTE-IDENTICAL |
| `logmind timeline --check` (clean) | BYTE-IDENTICAL |
| `logmind file-structure --check` (clean) | BYTE-IDENTICAL |
| `logmind tree` | BYTE-IDENTICAL |
| `logmind timeline --check` (stale) | BYTE-IDENTICAL (exit 1) |
| `logmind rebase` (failure path on dirty WT) | BYTE-IDENTICAL |

Confirmation method: `diff <(venv/bin/logmind <cmd>) <(bin/logmind
<cmd>)` against `/Users/ludlow/logmind` itself — the largest real
fixture available.

## Snapshot tests (49 total + 7 golden fixtures)

- **`internal/timeline/`** — 4 goldens covering brief/full ×
elision-vs-not, plus the empty/single-newline property tests
- `brief-mixed.golden` — 3 months (4-entry elision, 2-entry verbatim,
1-entry singleton)
- `brief-singular-elision.golden` — exactly 3 entries → `... 1 more
decision ...` singular noun
- `brief-two-elided-months.golden` — two elided months back-to-back for
inter-month spacing
- `full-mixed.golden` — full mode pins lack of count suffix on month
headers
- **`internal/tree/`** — `generate-file-structure.golden` pins the
template head + tail bytes
- **`internal/cli/`** — `timeline_stdout_brief.golden`,
`timeline_stdout_full.golden` for end-to-end stdout shape
- 23 cli tests + 26 package tests, all green via `make test`

## Brief-mode byte-identical proof

Brief-mode month grouping is the highest-risk algorithm — Python uses
`lines = [HEADER, ""]` then `"\\n".join(lines)`, which produces three
newlines between the `---` footer of the header and the first `##
YYYY-MM`. Go's `strings.Builder` byte-level approach would emit only two
unless you mirror the join semantics precisely.

Resolution: assemble a `[]string` mirror of Python's `lines` and use
`strings.Join(lines, "\\n")` rather than emitting bytes directly. The
trailing `lines.append("")` gives the final `\\n` after the last entry.
See `internal/timeline/timeline.go` comments for the trace.

Verified against the actual `docs/timeline.md` which carries the
brief-mode elision lines for 6 months of real history.

## Tree-walk byte-identical proof

Tree walk uses pure Go (`filepath.WalkDir` not invoked — explicit
`os.ReadDir` per frame for sort control). Sort key matches Python: `(not
is_dir, name.lower())` → directories first then case-insensitive
alphabetical. Pattern matching is path-aware (matches full relative
path, components, and basename).

One known semantic divergence: `filepath.Match`'s `*` does NOT cross
`/`, but Python's `fnmatch.fnmatchcase`'s `*` DOES. For the patterns
logmind actually ships (`DEFAULT_IGNORES` + typical `.gitignore`), this
is invisible — patterns are either segment-level (`*.pyc`,
`__pycache__`) or fully literal (`site/.next`). Documented in
`patternSetMatches`.

Verified at depth 0/1/2/3 against the logmind repo itself which is the
most realistic fixture I have (it has `site/.next/`, `venv/`,
`__pycache__/`, `.git/`, mixed casing, et al).

## Known divergence vs Python v0.6.14

`--check` without `--write`:
- Python: prints error + exits **2**
- Go: prints byte-identical error + exits **1**

The stdout message is unchanged so consumers diffing output see no
difference. The exit-code divergence is a documented known issue.
Closing it would require an `ErrSilentExit2` sentinel in
`cmd/logmind/main.go` (deferred to a coordinated cross-wave change — B4
lands first and is a candidate).

## Open questions for human reviewer

1. **Exit-code divergence**: keep as documented divergence, or land the
`ErrSilentExit2` sentinel as a coordinated patch alongside B4?
2. **Glob semantics divergence**: the `*-vs-/` divergence is theoretical
but real. Want a custom fnmatch port that's `*=anything-including-/`
strict, or accept the practical equivalence?
3. **Rebase `--no-fetch`**: Python exposes `--no-fetch` so tests can
rebase against a fixture without an `origin/`. I ported it for parity;
any objection?
4. **B4 coexistence**: this PR is independent of B4 work happening in
parallel on `feat/go-b4-agents`. The B4 branch will need to add
`root.AddCommand(newAgentsCmd())` next to the B3 wires.

## Design decisions

- **Brief-mode algorithm**: assemble `[]string` mirror + `strings.Join`
rather than byte-level emission. See comment in
`internal/timeline/timeline.go:106-119`.
- **Tree walker is pure Go**: never shell out to system `tree(1)` even
when available. Consuming repos get identical output across OSes. See
`internal/tree/tree.go:1-22`.
- **Config loader strategy**: typed `Config` struct with `yaml:` tags;
user keys overlay defaults leaf-by-leaf via `yaml.Unmarshal` into a
pre-populated default struct. Mirrors Python's `_deep_update` shape
without recursive Map traversal in Go. See
`internal/config/config.go:118-140`.
- **`-1 sentinel` for unbounded depth**: CLI's `--max-depth 0`
translates to internal `-1` so `Render` can use a simple `maxDepth >= 0
&& depth >= maxDepth` guard. Documented at the function boundary.
- **DefaultBranch 5-step search**: extends `internal/gitcli` (not
duplicated in `internal/rebase`) because B5 (init) and B4 (agents in
some paths) will also want it.

## Test plan

- [x] `make build && make test` — all packages green
- [x] `make snapshot` regenerates 7 golden files without drift
- [x] Byte-identical diff vs Python v0.6.14 across 8+ invocation shapes
on the logmind repo itself
- [x] `--check` stale + clean paths verified
- [x] `rebase` failure paths verified (not-a-repo, detached HEAD,
refusing-self, fetch fail, rebase fail)
- [ ] CI matrix (Go 1.22+) — will run on push
- [ ] Manual sanity check by reviewer on a consuming repo (e.g.
tokenomics) to confirm the merge driver still converges

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: thrillmot <thrillmot@users.noreply.github.com>
@thrillmot thrillmot mentioned this pull request Jun 3, 2026
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant