From 3e199b262f3914a935188afd40b73b83f0de3f21 Mon Sep 17 00:00:00 2001 From: Roman Borodavkin Date: Mon, 28 Sep 2026 22:14:18 +0300 Subject: [PATCH 1/2] chore(go): require Go 1.27 Keep the module minimum, CI toolchains, and lint version aligned so dependency updates are checked with the compiler version this repository supports. 1. Toolchain: require Go 1.27, use Go 1.27.1 in CI and CodeQL, and pin golangci-lint 2.14.0. 2. Compatibility: apply the Go 1.27 struct literal and string idioms reported by the updated linter without changing runtime behavior. 3. Docs and checks: update the installation requirement, test comment, checksums, and README formatting. Impact: local builds and CI consistently require Go 1.27 or newer. --- .github/workflows/build.yml | 7 ++-- .github/workflows/codeql.yml | 2 +- README.md | 66 ++++++++++++++++---------------- bear/domain/canonical.go | 2 +- bear/domain/routing.go | 2 +- cmd/noxctl/daemon.go | 22 +++++------ go.mod | 4 +- go.sum | 5 +-- tests/bear/config/daemon_test.go | 2 +- tests/bear/engine/daemon_test.go | 14 +++---- 10 files changed, 59 insertions(+), 67 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0f569a..bd46ec8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: '1.26.2' + GO_VERSION: '1.27.1' jobs: # ── Tier 1: Fast file-level checks (no toolchain, seconds) ── @@ -127,9 +127,8 @@ jobs: cache: true - uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 with: - # v2.12.2 is built with Go 1.26.2 — earlier 2.0.x was built - # with Go 1.24 and refused to lint a Go-1.26-targeted repo. - version: v2.12.2 + # Keep the linter's build toolchain at least as new as go.mod. + version: v2.14.0 args: --timeout=5m # ── Tier 3: Unit + integration tests (parallel with lint) ── diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ea25242..2d32fb0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -44,7 +44,7 @@ jobs: - if: matrix.language == 'go' uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v6.0.0 with: - go-version: '1.26.2' + go-version: '1.27.1' cache: true - uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 diff --git a/README.md b/README.md index 3e04a7e..bd5100f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -

- noxctl — declarative Bear notes for the terminal -

+noxctl — declarative Bear notes for the terminal # noxctl @@ -60,8 +58,8 @@ For each managed tag, noxctl writes two things to Bear: Atoms keep their human-authored body; noxctl only owns the canonical line at the top and the master/hub layout. Here is one `#library/books` tag in Bear, before and after a first `noxctl apply`: -| Before | After | -|-----------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| Before | After | +|-----------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------| | ![Bear filtered to #nox-demo/books before apply: five atom notes, no master](docs/screenshots/before.png) | ![Bear filtered to #nox-demo/books after apply: master ✱ Books plus five atoms, canonical tag-line chips on each](docs/screenshots/after.png) | (Demo vault is at `examples/demo-vault/` — `setup.sh` populates it under `#nox-demo/books` and the paired `noxctl.toml` manages exactly that tag.) @@ -132,7 +130,7 @@ noxctl has two entry paths that share the same install and the same `validate **Before you start.** - You need macOS with [Bear](https://bear.app/) installed. noxctl talks to Bear through Bear's bundled `bearcli`. -- You need Go 1.26 or newer: +- You need Go 1.27 or newer: ```bash go version ``` @@ -224,13 +222,13 @@ Bear ships a built-in backup in **File → Backup Database…** — recommended **How those map to a blueprint:** -| Observed shape | Recommendation | -|-------------------------------------------------|-----------------------------------------------------------| -| No usable bucket signal | `flat-list` (high confidence when small, medium when large) | -| Top-level tag, few atoms per bucket | `grouped-vertical` (alternative: `hub-routed-with-subtag`) | -| Top-level tag, many atoms per bucket | `hub-routed-with-subtag` (alternative: `grouped-vertical`) | -| Nested tag, strong author signal or many buckets| `hub-routed` | -| Nested tag, a small declared bucket set | `grouped-vertical` | +| Observed shape | Recommendation | +|--------------------------------------------------|-------------------------------------------------------------| +| No usable bucket signal | `flat-list` (high confidence when small, medium when large) | +| Top-level tag, few atoms per bucket | `grouped-vertical` (alternative: `hub-routed-with-subtag`) | +| Top-level tag, many atoms per bucket | `hub-routed-with-subtag` (alternative: `grouped-vertical`) | +| Nested tag, strong author signal or many buckets | `hub-routed` | +| Nested tag, a small declared bucket set | `grouped-vertical` | This follows the *Choosing a blueprint* decision tree, with one extra shortcut the eyeball version omits: on a nested tag, a high distinct-bucket count alone is enough for `hub-routed` (even without an author signal), so the master stays a scannable list of hubs. @@ -345,10 +343,10 @@ Exit-code shape: noxctl has two operating modes. Start with the one-shot mode until you trust the plan output on your own vault. -| Mode | What it does | When to use it | Persistence | -|------|--------------|----------------|-------------| -| `validate → plan → apply` | Runs once, shows a diff, then applies it only when you ask | First runs, careful changes, manual control | Exits after the command finishes | -| `daemon` | Runs continuously and reconciles managed structures when Bear changes | Mature configs you want kept in sync automatically | Lives only while the process is running unless you install a LaunchAgent | +| Mode | What it does | When to use it | Persistence | +|---------------------------|-----------------------------------------------------------------------|----------------------------------------------------|--------------------------------------------------------------------------| +| `validate → plan → apply` | Runs once, shows a diff, then applies it only when you ask | First runs, careful changes, manual control | Exits after the command finishes | +| `daemon` | Runs continuously and reconciles managed structures when Bear changes | Mature configs you want kept in sync automatically | Lives only while the process is running unless you install a LaunchAgent | Run the daemon manually when testing continuous reconciliation: @@ -411,15 +409,15 @@ launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.barad1tos.noxctl.plist noxctl is useful when Bear is still the place you want to write, but manual structure maintenance has become the annoying part. -| You gain | You trade away | Why it matters | -|----------|----------------|----------------| -| Generated index/hub notes | Some generated structure is now tool-owned | Edit your content freely, but do not hand-tune generated masters/hubs and expect those edits to survive regeneration | -| Reviewable `plan` output before writes | You need to understand the plan before applying it | Safer than silent automation, but still requires attention | -| Readiness and verification gates | A few extra commands before the first write | `doctor` catches local setup issues before mutation; `verify` confirms the vault and daemon are clean afterward | -| Consistent structure across many tags | A config file becomes part of your note system | Your organization rules live in `noxctl.toml`, not only in your head | -| Optional live reconciliation | A long-running process may be running on your Mac | Great for stable configs, but start manually before installing a LaunchAgent | -| Obsidian-like organization depth inside Bear | macOS + Bear + terminal are required | This is for Bear power users on macOS, not a cross-platform no-code workflow | -| Faster cleanup of drift | Bad config can produce bad structure quickly | Always back up before first use and inspect `plan` output | +| You gain | You trade away | Why it matters | +|----------------------------------------------|----------------------------------------------------|----------------------------------------------------------------------------------------------------------------------| +| Generated index/hub notes | Some generated structure is now tool-owned | Edit your content freely, but do not hand-tune generated masters/hubs and expect those edits to survive regeneration | +| Reviewable `plan` output before writes | You need to understand the plan before applying it | Safer than silent automation, but still requires attention | +| Readiness and verification gates | A few extra commands before the first write | `doctor` catches local setup issues before mutation; `verify` confirms the vault and daemon are clean afterward | +| Consistent structure across many tags | A config file becomes part of your note system | Your organization rules live in `noxctl.toml`, not only in your head | +| Optional live reconciliation | A long-running process may be running on your Mac | Great for stable configs, but start manually before installing a LaunchAgent | +| Obsidian-like organization depth inside Bear | macOS + Bear + terminal are required | This is for Bear power users on macOS, not a cross-platform no-code workflow | +| Faster cleanup of drift | Bad config can produce bad structure quickly | Always back up before first use and inspect `plan` output | A good first test is one low-risk tag with a small number of notes. Import it, review the generated config, run `plan`, read the diff, back up Bear, and only then run `apply`. @@ -439,13 +437,13 @@ Five rendering blueprints, each fitting a distinct tag shape. Walk the decision
Full comparison table & structure diagram -| Blueprint | When to use | Required fields beyond the basics | Bucket source | Output shape | -|--------------------------|-----------------------------------------------------------------|-----------------------------------|------------------------------------------|---------------------------------------------------| -| `flat-list` | inbox / capture tags, no grouping | none | n/a | one master with bullet list of every atom | -| `grouped-vertical` | bucketed collection in one master, operator-declared buckets | `buckets`, `unknown_bucket` | operator-declared (sub-tag or canonical 3rd segment, auto by tag depth) | one master with `## Bucket (N)` H2 per bucket | -| `hub-routed` | author / source grouping where bucket names live in atom bodies | `unknown_bucket`, `hub_h2_prefix` | atom body H2 (author/source), stamped into the canonical line | Tier-2: master lists hubs, each hub lists atoms | -| `hub-routed-with-subtag` | hub-style layout but bucket names are sub-tags | `buckets`, `unknown_bucket` | atom sub-tag `#tag/bucket` | Tier-2: master lists hubs, each hub lists atoms | -| `umbrella` | aggregate multiple existing domains under one master | `children`, `default_child` | n/a | master lists every child domain | +| Blueprint | When to use | Required fields beyond the basics | Bucket source | Output shape | +|--------------------------|-----------------------------------------------------------------|-----------------------------------|-------------------------------------------------------------------------|-------------------------------------------------| +| `flat-list` | inbox / capture tags, no grouping | none | n/a | one master with bullet list of every atom | +| `grouped-vertical` | bucketed collection in one master, operator-declared buckets | `buckets`, `unknown_bucket` | operator-declared (sub-tag or canonical 3rd segment, auto by tag depth) | one master with `## Bucket (N)` H2 per bucket | +| `hub-routed` | author / source grouping where bucket names live in atom bodies | `unknown_bucket`, `hub_h2_prefix` | atom body H2 (author/source), stamped into the canonical line | Tier-2: master lists hubs, each hub lists atoms | +| `hub-routed-with-subtag` | hub-style layout but bucket names are sub-tags | `buckets`, `unknown_bucket` | atom sub-tag `#tag/bucket` | Tier-2: master lists hubs, each hub lists atoms | +| `umbrella` | aggregate multiple existing domains under one master | `children`, `default_child` | n/a | master lists every child domain | Required fields beyond the basics — every blueprint also needs `tag`, `index_title`, `blueprint`. See `examples/.toml` for a copy-pasteable starter per blueprint. @@ -521,7 +519,7 @@ See `examples/minimal.toml` for a tested starter and `examples/personal.toml` fo ## Status & scope - **Platform:** macOS only. Bear is macOS-only; the watcher uses FSEvents via `fsnotify`'s Darwin backend; the CLI bridge is `bearcli` at `/Applications/Bear.app/Contents/MacOS/bearcli`. -- **Runtime:** Go ≥ 1.26. Direct dependencies are intentionally small: TOML parsing, Cobra CLI wiring, fsnotify for the daemon watcher, and a small set of Go `x/*` support packages. Adding a runtime dependency is deliberate and requires justification. +- **Runtime:** Go ≥ 1.27. Direct dependencies are intentionally small: TOML parsing, Cobra CLI wiring, fsnotify for the daemon watcher, and a small set of Go `x/*` support packages. Adding a runtime dependency is deliberate and requires justification. - **Heritage:** descended from `regen-watchd`, a personal FSEvents daemon that managed a 28-domain Bear corpus; the closed catalog of five blueprints covers every shape that production used. - **Acceptance test:** byte-equivalent vault output against the legacy daemon for the maintainer's 28-domain corpus. - **License:** MIT. diff --git a/bear/domain/canonical.go b/bear/domain/canonical.go index 02564a7..c19491e 100644 --- a/bear/domain/canonical.go +++ b/bear/domain/canonical.go @@ -103,7 +103,7 @@ func (s *atomicParseState) consumeLeadingBlank(trimmed string, p *AtomicParts) b // switch documents that order matters (H1 before header before blank-skip). func (d *Domain) parseAtomicContent(content, author string) AtomicParts { authorH2Marker := "## " + author - family := strings.SplitN(d.Tag, "/", 2)[0] + family, _, _ := strings.Cut(d.Tag, "/") var parts AtomicParts state := atomicParseState{stripAuthor: d.StripLegacyAuthorH2} for line := range strings.SplitSeq(content, "\n") { diff --git a/bear/domain/routing.go b/bear/domain/routing.go index 91f7f64..ecb3096 100644 --- a/bear/domain/routing.go +++ b/bear/domain/routing.go @@ -469,7 +469,7 @@ func (d *Domain) computeTagOverrides(notes []Note) (overrides map[string]string, if d.CanonicalTagFor == nil { return nil, 0 } - family := strings.SplitN(d.Tag, "/", 2)[0] + family, _, _ := strings.Cut(d.Tag, "/") prefix := family + "/" for _, note := range notes { if !hasFamilyMembership(note.Tags, family) { diff --git a/cmd/noxctl/daemon.go b/cmd/noxctl/daemon.go index 9cffb70..517da27 100644 --- a/cmd/noxctl/daemon.go +++ b/cmd/noxctl/daemon.go @@ -85,18 +85,16 @@ func runDaemon(cmd *cobra.Command, _ []string) error { return fmt.Errorf(errFmtNoxctlDaemon, bearDBErr) } opts := engine.DaemonOpts{ - ApplyOpts: engine.ApplyOpts{ - Domains: domains, - Pins: pins, - StatePath: dc.StatePath, - LockPath: dc.LockPath, - Features: cliutil.ResolveFeatures(cat, dc), - AuditEnabled: dc.AuditEnabled, - BearcliConcurrency: dc.BearcliConcurrency, - Stderr: os.Stderr, - DailyDefaultTag: dailyDefaultTagFromCatalog(cat), - PromotionRules: promotionRulesFromCatalog(cat), - }, + Domains: domains, + Pins: pins, + StatePath: dc.StatePath, + LockPath: dc.LockPath, + Features: cliutil.ResolveFeatures(cat, dc), + AuditEnabled: dc.AuditEnabled, + BearcliConcurrency: dc.BearcliConcurrency, + Stderr: os.Stderr, + DailyDefaultTag: dailyDefaultTagFromCatalog(cat), + PromotionRules: promotionRulesFromCatalog(cat), BearDBDir: bearDBDir, DebouncePause: dc.DebouncePause, MaxBurstWindow: dc.MaxBurstWindow, diff --git a/go.mod b/go.mod index b23b2bf..e3d6dc2 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/barad1tos/noxctl -go 1.26.2 +go 1.27 require ( github.com/BurntSushi/toml v1.6.0 github.com/fsnotify/fsnotify v1.10.1 github.com/spf13/cobra v1.10.2 - golang.org/x/sys v0.48.0 golang.org/x/sync v0.23.0 + golang.org/x/sys v0.48.0 ) require ( diff --git a/go.sum b/go.sum index 09e7222..0260809 100644 --- a/go.sum +++ b/go.sum @@ -12,9 +12,8 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= - +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/tests/bear/config/daemon_test.go b/tests/bear/config/daemon_test.go index a019ec6..23065f1 100644 --- a/tests/bear/config/daemon_test.go +++ b/tests/bear/config/daemon_test.go @@ -34,7 +34,7 @@ type loadDaemonCase[V any] struct { // // Closure-capture safety: `c` is captured by the `t.Run` closure, which // would alias to the last iteration on Go < 1.22. The module pins -// `go 1.26.2` in go.mod, so per-iteration scoping is guaranteed by the +// `go 1.27` in go.mod, so per-iteration scoping is guaranteed by the // language spec and no defensive `c := c` shadow is needed. func runLoadDaemonCases[V any]( t *testing.T, diff --git a/tests/bear/engine/daemon_test.go b/tests/bear/engine/daemon_test.go index baae52c..d0c9a7e 100644 --- a/tests/bear/engine/daemon_test.go +++ b/tests/bear/engine/daemon_test.go @@ -80,14 +80,12 @@ func newTestDaemonOpts(t *testing.T) engine.DaemonOpts { t.Helper() dir := t.TempDir() return engine.DaemonOpts{ - ApplyOpts: engine.ApplyOpts{ - Domains: nil, - Pins: nil, - StatePath: filepath.Join(dir, "state.json"), - LockPath: filepath.Join(dir, ".lock"), - Features: engine.Features{}, // all false — no bearcli traffic - Stderr: os.Stderr, - }, + Domains: nil, + Pins: nil, + StatePath: filepath.Join(dir, "state.json"), + LockPath: filepath.Join(dir, ".lock"), + Features: engine.Features{}, // all false — no bearcli traffic + Stderr: os.Stderr, BearDBDir: dir, DebouncePause: 50 * time.Millisecond, MaxBurstWindow: 500 * time.Millisecond, From dceec0ad1cbf87aef49ad7b6ca8ae6652e175320 Mon Sep 17 00:00:00 2001 From: Roman Borodavkin Date: Mon, 28 Sep 2026 23:01:59 +0300 Subject: [PATCH 2/2] docs: require Go 1.27 in contributor setup --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1057378..0b4fe77 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ By participating, you agree to uphold the [Code of Conduct](.github/CODE_OF_COND ## Getting started 1. Fork the repository and clone it locally. -2. Make sure you have **Go ≥ 1.26** installed (`go version`). +2. Make sure you have **Go ≥ 1.27** installed (`go version`). 3. Install the local hooks once: ```bash pre-commit install