fix: allow concept names as camp go navigation targets#208
fix: allow concept names as camp go navigation targets#208lancekrogers merged 7 commits intomainfrom
Conversation
BuildCategoryMappings only mapped shortcut keys (d, ai, f) to categories, so `cgo docs` and `cgo ai_docs` failed even though those are valid path concept names defined in jumps.yaml. Capture the raw paths map from jumps.yaml during loading (PathsMap) and feed it into BuildCategoryMappings so concept names resolve from the config file directly — no hardcoded struct-to-name mapping needed. Shortcuts take precedence on collision.
262ddb2 to
0f7f9f7
Compare
obey-agent
left a comment
There was a problem hiding this comment.
Verdict: Request Changes
Overview:
The direction is right and it fixes the happy path, but the alias map still does not reflect the effective campaign paths after normalization and defaulting. That leaves partial and legacy jumps.yaml configs with missing concept-name resolution even though the rest of the settings pipeline normalizes them.
Key Findings:
internal/config/types.go:PathsMap()returns raw YAML state instead of the normalized/defaulted config, so concept-name aliases can still be absent for legacy or partial settings.
What's Done Well:
- The call sites were updated consistently.
- The UX improvement is well scoped and the added tests are pointed at the intended behavior.
Staff Standard:
Not yet. I would want the effective config and the alias map to agree before merging.
obey-agent
left a comment
There was a problem hiding this comment.
Verdict: Approve
Follow-up:
The effective-path alias mismatch is fixed. PathsMap now stays aligned with the normalized/defaulted jumps config, so concept-name navigation no longer diverges for legacy or partial settings.
obey-agent
left a comment
There was a problem hiding this comment.
Staff-Level Review — Request Changes
Verdict: Request Changes
Overview: The intent is sound and the BuildCategoryMappings(shortcuts, pathsMap) signature change is the right shape. The implementation, however, is leaky at two important seams — map aliasing in SaveJumpsConfig, and incomplete coverage for partial jumps.yaml files at load time. The new unit test sidesteps the real production path, so the bug the PR is meant to prevent can still reach users through LoadJumpsConfig call sites that don't follow up with ApplyDefaults.
Key Findings
-
SaveJumpsConfigaliases the caller'sPathsMap. The shallow copynormalized := *cfgshares the map, andnormalized.refreshPathsMap()mutates it as a side effect. The block immediately above explicitly deep-copiesShortcutsfor exactly this reason — the handling is now inconsistent. The refresh call is also inert for serialization (PathsMapisyaml:"-"andnormalizedis discarded afteryaml.Marshal), so the cleanest fix is to dropnormalized.refreshPathsMap()from Save. See inline atinternal/config/settings.go:111. -
LoadJumpsConfigdoesn't apply defaults. Partial jumps.yaml → partialcfg.Paths→asMap()skips empty fields → partialPathsMap. Users with an old/hand-edited config hit the same fuzzy-search fallthrough the PR is trying to eliminate. The mainLoadCampaignConfig→loadJumpspath compensates by callingApplyDefaultsafter the loader returns, but the three direct call sites incmd/camp/navigation/shortcuts.go(518, 732, 855) don't. Movecfg.ApplyDefaults()intoLoadJumpsConfigbeforerefreshPathsMap()so the invariant lives in one place. See inline atinternal/config/settings.go:86. -
asMap()doc comment contradicts the code. "Only used when no jumps.yaml is present" is wrong —refreshPathsMapcalls it on every load. This matters because the comment sets the wrong mental model for the next maintainer. See inline atinternal/config/types.go:160. -
Test coverage gap.
TestJumpsConfigRefreshPathsMapUsesEffectivePathshand-constructs aJumpsConfigand explicitly callsApplyDefaults(), which is precisely the step that's missing from the real load path. Please add a test that writes a partial jumps.yaml (e.g. onlypaths: {docs: ...}) to a tempdir, round-trips it throughLoadJumpsConfig, and asserts that bothprojectsanddocsresolve — that's the class of regression the PR is meant to prevent.
What's Done Well
- Layering of concept names beneath explicit shortcuts in
BuildCategoryMappingsis the right precedence and is documented in the function comment. - Accepting
nilforpathsMapkeeps the existing call sites and test fixtures ergonomic. - Preserving custom aliases via the raw-map second
yaml.Unmarshalis a tasteful way to let users keep non-standard path keys without adding struct fields. - Test and production call-site updates are consistent (
cfg.Shortcuts(), cfg.PathsMap()), and the completion count assertions were updated correctly to match the new 11+11 mapping fan-out.
Staff Standard
I would not sign off on this as my own work in its current state. The aliasing bug in SaveJumpsConfig is the kind of subtle defect that surfaces as a flaky test or a user-reported "why did my PathsMap grow unexpectedly?" months down the line, and the loader coverage gap undermines the PR's stated goal for a real class of users. Both are fixable in a small follow-up commit; happy to re-review once the three inline comments are addressed.
obey-agent
left a comment
There was a problem hiding this comment.
Staff-Level Review — Request Changes
Verdict: Request Changes
Overview: The intent is sound and the BuildCategoryMappings(shortcuts, pathsMap) signature change is the right shape. The implementation, however, is leaky at two important seams — map aliasing in SaveJumpsConfig, and incomplete coverage for partial jumps.yaml files at load time. The new unit test sidesteps the real production path, so the bug the PR is meant to prevent can still reach users through LoadJumpsConfig call sites that don't follow up with ApplyDefaults.
Key Findings
-
SaveJumpsConfigaliases the caller'sPathsMap. The shallow copynormalized := *cfgshares the map, andnormalized.refreshPathsMap()mutates it as a side effect. The block immediately above explicitly deep-copiesShortcutsfor exactly this reason — the handling is now inconsistent. The refresh call is also inert for serialization (PathsMapisyaml:"-"andnormalizedis discarded afteryaml.Marshal), so the cleanest fix is to dropnormalized.refreshPathsMap()from Save. See inline atinternal/config/settings.go:111. -
LoadJumpsConfigdoesn't apply defaults. Partial jumps.yaml → partialcfg.Paths→asMap()skips empty fields → partialPathsMap. Users with an old/hand-edited config hit the same fuzzy-search fallthrough the PR is trying to eliminate. The mainLoadCampaignConfig→loadJumpspath compensates by callingApplyDefaultsafter the loader returns, but the three direct call sites incmd/camp/navigation/shortcuts.go(518, 732, 855) don't. Movecfg.ApplyDefaults()intoLoadJumpsConfigbeforerefreshPathsMap()so the invariant lives in one place. See inline atinternal/config/settings.go:86. -
asMap()doc comment contradicts the code. "Only used when no jumps.yaml is present" is wrong —refreshPathsMapcalls it on every load. This matters because the comment sets the wrong mental model for the next maintainer. See inline atinternal/config/types.go:160. -
Test coverage gap.
TestJumpsConfigRefreshPathsMapUsesEffectivePathshand-constructs aJumpsConfigand explicitly callsApplyDefaults(), which is precisely the step that's missing from the real load path. Please add a test that writes a partial jumps.yaml (e.g. onlypaths: {docs: ...}) to a tempdir, round-trips it throughLoadJumpsConfig, and asserts that bothprojectsanddocsresolve — that's the class of regression the PR is meant to prevent.
What's Done Well
- Layering of concept names beneath explicit shortcuts in
BuildCategoryMappingsis the right precedence and is documented in the function comment. - Accepting
nilforpathsMapkeeps the existing call sites and test fixtures ergonomic. - Preserving custom aliases via the raw-map second
yaml.Unmarshalis a tasteful way to let users keep non-standard path keys without adding struct fields. - Test and production call-site updates are consistent (
cfg.Shortcuts(), cfg.PathsMap()), and the completion count assertions were updated correctly to match the new 11+11 mapping fan-out.
Staff Standard
I would not sign off on this as my own work in its current state. The aliasing bug in SaveJumpsConfig is the kind of subtle defect that surfaces as a flaky test or a user-reported "why did my PathsMap grow unexpectedly?" months down the line, and the loader coverage gap undermines the PR's stated goal for a real class of users. Both are fixable in a small follow-up commit; happy to re-review once the three inline comments are addressed.
obey-agent
left a comment
There was a problem hiding this comment.
Staff-Level Review — Request Changes
Verdict: Request Changes
Overview: The intent is sound and the BuildCategoryMappings(shortcuts, pathsMap) signature change is the right shape. The implementation, however, is leaky at two important seams — map aliasing in SaveJumpsConfig, and incomplete coverage for partial jumps.yaml files at load time. The new unit test sidesteps the real production path, so the bug the PR is meant to prevent can still reach users through LoadJumpsConfig call sites that don't follow up with ApplyDefaults.
Key Findings
-
SaveJumpsConfigaliases the caller'sPathsMap. The shallow copynormalized := *cfgshares the map, andnormalized.refreshPathsMap()mutates it as a side effect. The block immediately above explicitly deep-copiesShortcutsfor exactly this reason — the handling is now inconsistent. The refresh call is also inert for serialization (PathsMapisyaml:"-"andnormalizedis discarded afteryaml.Marshal), so the cleanest fix is to dropnormalized.refreshPathsMap()from Save. See inline atinternal/config/settings.go:111. -
LoadJumpsConfigdoesn't apply defaults. Partial jumps.yaml → partialcfg.Paths→asMap()skips empty fields → partialPathsMap. Users with an old/hand-edited config hit the same fuzzy-search fallthrough the PR is trying to eliminate. The mainLoadCampaignConfig→loadJumpspath compensates by callingApplyDefaultsafter the loader returns, but the three direct call sites incmd/camp/navigation/shortcuts.go(518, 732, 855) don't. Movecfg.ApplyDefaults()intoLoadJumpsConfigbeforerefreshPathsMap()so the invariant lives in one place. See inline atinternal/config/settings.go:86. -
asMap()doc comment contradicts the code. "Only used when no jumps.yaml is present" is wrong —refreshPathsMapcalls it on every load. This matters because the comment sets the wrong mental model for the next maintainer. See inline atinternal/config/types.go:160. -
Test coverage gap.
TestJumpsConfigRefreshPathsMapUsesEffectivePathshand-constructs aJumpsConfigand explicitly callsApplyDefaults(), which is precisely the step that's missing from the real load path. Please add a test that writes a partial jumps.yaml (e.g. onlypaths: {docs: ...}) to a tempdir, round-trips it throughLoadJumpsConfig, and asserts that bothprojectsanddocsresolve — that's the class of regression the PR is meant to prevent.
What's Done Well
- Layering of concept names beneath explicit shortcuts in
BuildCategoryMappingsis the right precedence and is documented in the function comment. - Accepting
nilforpathsMapkeeps the existing call sites and test fixtures ergonomic. - Preserving custom aliases via the raw-map second
yaml.Unmarshalis a tasteful way to let users keep non-standard path keys without adding struct fields. - Test and production call-site updates are consistent (
cfg.Shortcuts(), cfg.PathsMap()), and the completion count assertions were updated correctly to match the new 11+11 mapping fan-out.
Staff Standard
I would not sign off on this as my own work in its current state. The aliasing bug in SaveJumpsConfig is the kind of subtle defect that surfaces as a flaky test or a user-reported "why did my PathsMap grow unexpectedly?" months down the line, and the loader coverage gap undermines the PR's stated goal for a real class of users. Both are fixable in a small follow-up commit; happy to re-review once the three inline comments are addressed.
obey-agent
left a comment
There was a problem hiding this comment.
Verdict: Approve
Follow-up:
The current head fixes the remaining config-loader issues. LoadJumpsConfig now applies defaults before refreshing the effective alias map, direct callers no longer depend on ad hoc ApplyDefaults() calls, SaveJumpsConfig no longer mutates the caller's runtime-only PathsMap, and config coverage now exercises partial jumps.yaml plus caller-map non-mutation.
obey-agent
left a comment
There was a problem hiding this comment.
Staff-Level Re-Review — Approve
Verdict: Approve
Overview: All three inline issues from my prior "Request Changes" review are addressed at 425cbaea, and the fixes are exactly the right shape. The two new tests in settings_test.go (+66 lines) cover precisely the gaps I flagged — partial jumps.yaml round-tripped through the real LoadJumpsConfig path, and a defensive-copy assertion on SaveJumpsConfig's non-mutation of caller state.
How each prior finding landed
-
SaveJumpsConfigaliasing the caller'sPathsMap— fixed at its cleanest: thenormalized.refreshPathsMap()call is simply removed. Save now does exactly what serialization needs and nothing more. No defensive deep-copy workaround, no subtle aliasing behavior.TestSaveJumpsConfig_DoesNotMutateCallerPathsMapnails this directly by constructing acfgwith pre-populatedPathsMap, calling Save, then asserting the caller's map was not mutated (specifically that it did NOT gain theprojectsdefault from Save's side). Good, targeted coverage. -
LoadJumpsConfignot applying defaults — fixed by swappingcfg.refreshPathsMap()forcfg.ApplyDefaults(), which already callsrefreshPathsMapinternally as its last step. This collapses two invariants into one call and makes the loader self-sufficient. Consequence: the redundantjumps.ApplyDefaults()calls incampaign.go:loadJumpsandinternal/project/worktrees_path.goare removed (−1 line each), so the "apply defaults" invariant now lives in one place instead of three. That's architecturally cleaner than my original suggestion.TestLoadJumpsConfig_AppliesDefaultsAndPreservesRawAliaseswrites a real partial YAML to a tempdir with onlyintentsand a customdocumentationalias, round-trips it throughLoadJumpsConfig, and asserts: (a)intentsgets normalized to.campaign/intents/byNormalizeIntentNavigation, (b)projectsgets populated fromApplyDefaults, and (c) the customdocumentationalias survives untouched. That's exactly the "partial jumps.yaml through the real loader" path I asked for. -
asMap()doc comment contradicting actual usage — fixed. New text: "Returns the canonical concept-name→path map derived from the current CampaignPaths struct values. JumpsConfig.refreshPathsMap uses it to keep the effective alias map aligned with normalized and defaulted runtime paths." That's accurate, names the caller, and sets the correct mental model for the next maintainer.
What's Still Done Well
- The fix uses less code than the previous revision: one call-site removed from Save, two redundant
ApplyDefaultscalls removed from downstream callers, and the load path collapses toNormalizeIntentNavigation → ApplyDefaultswith ApplyDefaults doing the rest. Subtractive. - Test coverage is targeted at the exact regression classes I flagged, not speculative adjacent cases. Each test is small (~30 lines) and asserts one invariant.
- The
asMapdoc update namesJumpsConfig.refreshPathsMapdirectly as the caller, so a reader following the code can jump straight to the consumer.
Staff Standard
Approve. This is how review feedback should be addressed: the fix is smaller than the original code, the tests cover the exact gaps I flagged, and the architectural consequence (one-place ApplyDefaults invariant) is better than my original suggestion of "add ApplyDefaults inside Load but leave the downstream calls intact." Ship it.
internal/config/types.go
Outdated
| // asMap returns the canonical concept-name→path map derived from the current | ||
| // CampaignPaths struct values. JumpsConfig.refreshPathsMap uses it to keep the | ||
| // effective alias map aligned with normalized and defaulted runtime paths. | ||
| func (p CampaignPaths) asMap() map[string]string { |
There was a problem hiding this comment.
This is not the intended design, the intended design is that these default concepts we include in camp are documented in the yaml file, users can edit that file and the camp go jumping should work with their edits, or whatever is in that file. Jumping to concepts defined in that yaml should work just like with the shortcuts.
obey-agent
left a comment
There was a problem hiding this comment.
Addressed the navigation review gaps. camp go now resolves configured shortcut keys, long-form directory aliases, and configured concepts separately, and first-token drill syntax now works via design/... and de@... filesystem traversal with dynamic shell completion.
obey-agent
left a comment
There was a problem hiding this comment.
Staff-Level Re-Review — Substantial Architectural Change (still Approve, with two follow-ups)
Verdict: Approve
Commit 3033311 "fix: resolve cgo long-form aliases from config" is a much larger change than the title suggests — 985 lines across 16 files, introducing internal/nav/configured_navigation.go with a new layered resolution architecture. I want to flag that scope growth explicitly since my prior approval was for a narrower fix, and note a couple of observations on the new surface.
What changed
The PathsMap/asMap approach I previously approved has been replaced with a cleaner design:
ResolveConfiguredTarget— unified entry point with explicit resolution order: shortcuts → concepts → long-form path aliases.BuildPathAliasMappings— derives aliases from navigation shortcut target paths (e.g.workflow/design/→design) instead of from a separatePathsMapfield. This is a better single-source-of-truth design.NormalizeNavigationName— case-insensitive, trailing-slash-tolerant matching across all three layers.- Drill syntax —
p/query,p@querypatterns handled viasplitSlashDrillArgs/splitShortcutDrillArgs. BuildCategoryMappingssignature reverted to single-arg (nopathsMap), consistent with the new layering.
Architecturally this is cleaner than what I approved last round. The ambiguity handling in BuildPathAliasMappings (drop the alias rather than silently pick one) is the right default for user-safety.
Observations (non-blocking)
-
JumpsConfig.PathsMapis now dead state.CampaignConfig.PathsMap()was removed in this commit (types.go−33 lines), and theasMap()method was moved to package-privateeffectivePathsMapinsettings.go. ButJumpsConfig.PathsMapis still a struct field andrefreshPathsMapstill populates it. Nothing outside the package reads it anymore. Either delete the field +refreshPathsMap(tidy), or leave a comment explaining why it's kept around for backward-compat (if that's the intent). -
ResolveConfiguredTargetbuilds shortcut mappings twice. In the drill-check branch and again in the main branch:shortcutMappings := BuildCategoryMappings(cfg.Shortcuts()) // build 1 // ... shortcutMappings := BuildCategoryMappings(cfg.Shortcuts()) // build 2 (identical)
Same applies to
BuildPathAliasMappingsbeing re-derived insideresolveDrillTarget. For a CLI the micro-cost is trivial, but hoisting both to the top ofResolveConfiguredTargetwould be cleaner and future-proof if the shortcut map ever grows. -
PR scope — the PR title is "fix: allow concept names as camp go navigation targets" but this commit adds drill syntax, fuzzy sub-directory matching, and long-form-alias resolution. That's a feature expansion. Consider splitting these into a follow-up PR if the base fix has already proven stable, or updating the PR title/description so reviewers know to expect the broader scope.
What's Still Done Well
- The new code is well-tested: 136 lines of new tests in
configured_navigation_test.goplus 30 lines of integration coverage. - Explicit resolution order documented in the
ResolveConfiguredTargetdocstring. - Ambiguity handling is conservative and safe.
NormalizeNavigationNameunifies case/slash handling so there's one canonical normalization across the codebase.- Shell integration updated (
bash,fish,zsh) — the three shells stay in sync.
Staff Standard
Architecture is cleaner than what I approved last round — this is a net improvement. Ship it. The two observations above are follow-up cleanups, not blockers. The PR-scope comment is process feedback for next time, not a request to amend this PR.
|
Design note for the current head ( This PR now treats
The intent is to support the UX you described:
Drill behavior added on this head:
What changed in code:
Important
The design line here is:
|
internal/config/settings.go
Outdated
| // PathsMap is the effective name→path map for jumps paths. | ||
| // It starts with the raw YAML keys, then overlays the normalized/defaulted | ||
| // standard campaign paths so alias lookups stay aligned with runtime config. | ||
| PathsMap map[string]string `yaml:"-"` |
There was a problem hiding this comment.
Dead code. PathsMap is added to JumpsConfig with ~50 lines of implementation (raw YAML capture, refreshPathsMap, effectivePathsMap) plus ~100 lines of tests, but nothing in production code ever reads it. A repo-wide rg PathsMap only hits settings.go and settings_test.go. Long-form directory aliases are derived from cfg.Shortcuts() via BuildPathAliasMappings, not from PathsMap, so a user-supplied key like paths.documentation: docs/ in jumps.yaml will not create a navigable alias — despite what TestLoadJumpsConfig_AppliesDefaultsAndPreservesRawAliases appears to demonstrate.
If this is intended for future use, land it with the consumer or drop it for now. Shipping a dead field inflates surface area that future maintainers will have to reason about and keep in sync with the rest of the config layer.
internal/complete/complete.go
Outdated
| } | ||
|
|
||
| // shortcutKeys returns the keys from a shortcuts map. | ||
| func shortcutKeys(shortcuts map[string]nav.Category) []string { |
There was a problem hiding this comment.
Dead function. shortcutKeys is no longer called anywhere after the refactor (previously used by Generate and CategoryShortcuts, both of which now call nav.TopLevelNavigationNames directly). rg shortcutKeys shows only the declaration here. Remove it rather than leaving stale helpers — they confuse future readers trying to understand the data flow.
cmd/camp/navigation/go.go
Outdated
|
|
||
| matches := fuzzy.FilterMulti(names, query) | ||
| if len(matches) == 0 { | ||
| return "", fmt.Errorf("no directories matching %q in %s", query, strings.TrimRight(relativePath, "/")) |
There was a problem hiding this comment.
Use the project error framework. These three fmt.Errorf call sites (L435, L449, L464) are new production code, and the project's guideline is to wrap via camperrors (you already import it and use camperrors.Wrap on L451). The trailing /-trim formatting is fine, but a plain fmt.Errorf string is not wrappable by callers and loses observability — no type, no sentinel, no errors.Is target.
Minimum: use camperrors.Wrapf(..., "...") or introduce a sentinel like ErrNavigationPathNotFound in internal/errors and wrap it here. This matters because shell completion / scripted callers rely on being able to distinguish 'not found' from 'I/O failed'.
| // Resolution order: | ||
| // 1. Configured shortcut keys from jumps.yaml | ||
| // 2. Configured concept names from campaign.yaml | ||
| // 3. Long-form directory aliases derived from configured navigation shortcuts |
There was a problem hiding this comment.
Docstring disagrees with implementation. The doc claims the order is (1) shortcut keys → (2) concepts → (3) path aliases, but the actual flow is:
- Shortcut drill (
de@foo) viasplitShortcutDrillArgs - Shortcut keys (plain
de) - Slash drill (
design/foo) viasplitSlashDrillArgs→resolveDrillTarget - Concept names (
resolveDrillTargetfirst branch) - Path aliases derived from shortcuts (
resolveDrillTargetsecond branch)
Concept names shadow path aliases silently when both exist at the same normalized key. That precedence decision is reasonable but invisible — please encode it explicitly in the docstring so it doesn't get swapped by accident later. Also mention the @ and / drill syntaxes; they're the user-facing surface and should be visible here.
| } | ||
| } | ||
|
|
||
| shortcutMappings := BuildCategoryMappings(cfg.Shortcuts()) |
There was a problem hiding this comment.
Redundant work — compute once, branch twice. BuildCategoryMappings(cfg.Shortcuts()) is rebuilt twice on any input that contains @ (once in the drill branch at L102, then again unconditionally at L114). BuildPathAliasMappings is similarly recomputed inside resolveDrillTarget at L159, and yet a third time inside TopLevelNavigationNames. Each rebuild iterates every shortcut, does path.Base/strings.TrimRight, and allocates a fresh map.
This runs on every tab completion, so it's in a shell-latency-sensitive hot path (shell tab completion is gated on the 200ms complete.Timeout). Compute both maps once at the top of ResolveConfiguredTarget, or memoize them on *CampaignConfig. The ergonomics are also better — you won't have ambiguity collisions diverging between sites.
internal/complete/complete.go
Outdated
| candidates = append(candidates, shortcut) | ||
| // Add matching top-level navigation names first. | ||
| for _, name := range topLevelNames { | ||
| if strings.HasPrefix(strings.ToLower(name), strings.ToLower(query)) { |
There was a problem hiding this comment.
Normalization inconsistency. Everywhere else in this PR, navigation tokens go through NormalizeNavigationName (lowercase + trim trailing /). Here in completeAll, you roll your own strings.HasPrefix(strings.ToLower(name), strings.ToLower(query)) — which does NOT trim a trailing slash from the query.
Consequence: if a user has typed design/ and ResolveConfiguredTarget hasn't matched (because say concepts list is empty and there's no design path alias), this falls through to completeAll with query="design/". The top-level name stored in topLevelNames is "design" (no slash). Prefix match fails. No suggestions.
Use NormalizeNavigationName on both sides here, or better yet, do the normalization once up front and compare normalized-to-normalized consistently.
cmd/camp/navigation/go.go
Outdated
| configMappings := nav.BuildCategoryMappings(cfg.Shortcuts()) | ||
| // Resolve configured navigation from shortcuts, long-form directory aliases, | ||
| // and configured concepts. | ||
| resolved := nav.ResolveConfiguredTarget(cfg, args) |
There was a problem hiding this comment.
Wasted call on short-circuit paths. ResolveConfiguredTarget runs unconditionally here, but the next ~15 lines short-circuit out for the toggle keyword and custom-path navigation shortcuts. For those cases (every cgo t, every cgo foo where foo is a non-standard-path shortcut) we pay the full resolution cost — sort shortcut keys, build both mappings, parse — and throw the result away.
Move the resolved := nav.ResolveConfiguredTarget(cfg, args) call down to just above line 84 where it's actually consumed, so the short-circuits stay cheap.
internal/complete/complete.go
Outdated
| func listPathCandidates(ctx context.Context, absPath, prefix string) ([]string, error) { | ||
| entries, err := os.ReadDir(absPath) | ||
| if err != nil { | ||
| return nil, nil |
There was a problem hiding this comment.
Errors are swallowed silently. listPathCandidates, listPathCandidatesRich, completeSubdirectoryInPath, and completeSubdirectoryInPathRich all do if err != nil { return nil, nil } on os.ReadDir. That pattern is reasonable for 'missing dir in completion' but it also silently drops permission errors, I/O errors, and bad symlinks. When a user has a corrupted workflow dir or a permission issue, they'll see 'no completions' with zero diagnostic signal.
Differentiate: swallow os.IsNotExist, but bubble up (or at least log to a debug channel / campaign audit log) the real I/O failures. If we care enough to call ctx.Err() inside the loop, we should care enough to distinguish 'nothing to suggest' from 'I/O broke.'
internal/config/settings_test.go
Outdated
| } | ||
| } | ||
|
|
||
| func TestSaveJumpsConfig_DoesNotMutateCallerPathsMap(t *testing.T) { |
There was a problem hiding this comment.
Test name is misleading. This asserts SaveJumpsConfig does not mutate the caller's PathsMap, but it only 'passes' because SaveJumpsConfig never calls ApplyDefaults. ApplyDefaults → refreshPathsMap writes directly into j.PathsMap (settings.go:211-219), so any caller that runs LoadJumpsConfig then ApplyDefaults on their own config would see their PathsMap mutated.
Either (a) rename to TestSaveJumpsConfig_DoesNotApplyDefaults so the actual invariant is clear, or (b) make refreshPathsMap operate on a copy and assign atomically so the non-mutation property holds regardless of call order. Option (b) is friendlier to future callers.
|
|
||
| basePath := filepath.Join(campaignRoot, relativePath) | ||
| exactPath := filepath.Join(basePath, query) | ||
| if info, err := os.Stat(exactPath); err == nil && info.IsDir() { |
There was a problem hiding this comment.
Context cancellation gap. fuzzyResolveDirectory starts with a ctx.Err() check, but the caller resolveRelativePathNavigation does not. So the os.Stat(exactPath) on line 421, the fuzzyResolveDirectory call on line 427, the os.Stat(nestedPath) on line 432 — all proceed even if ctx is already cancelled/timed-out. For the shell-integration path, complete.Timeout is 200ms; a stale call here after timeout hits the disk anyway.
Please add a ctx.Err() gate at the top of resolveRelativePathNavigation and before each os.Stat (at least the second one). Stats are cheap but not free, and the project CLAUDE guidance is 'always check ctx.Err() before starting long operations.'
obey-agent
left a comment
There was a problem hiding this comment.
Verdict: Request Changes
Overview
This PR introduces a real improvement — letting users type concept names, long-form directory aliases, and drill syntax (design/foo, de@foo) at the first navigation arg. The core design (dedicated ResolveConfiguredTarget in internal/nav, shared between cgo and the completion engine) is sound and the test coverage for the happy paths is solid. But there are several issues that need cleanup before this ships.
Key Findings
JumpsConfig.PathsMapis dead code — 50+ lines of implementation plus ~100 lines of test, no production consumer.BuildPathAliasMappingsstill derives aliases fromcfg.Shortcuts(), not from the newPathsMap. See inline on settings.go:34.fmt.Errorfin new production paths — violates the project's stated error convention. See inline on go.go:464.- Dead function
shortcutKeysleft in complete.go after the refactor. See inline on complete.go:163. - Docstring in
ResolveConfiguredTargetdisagrees with the implementation — it describes 3 resolution steps, the code has 5 with non-trivial precedence choices (concept shadows alias, drill-form precedes plain). See inline on configured_navigation.go:94. - Redundant hot-path work —
BuildCategoryMappingsandBuildPathAliasMappingsrecomputed 2–3× per invocation in a shell-completion path gated on a 200ms timeout. See inline on configured_navigation.go:114. - Normalization inconsistency in
completeAll(customstrings.ToLowervsNormalizeNavigationNameeverywhere else) — breaks completion when a user types a trailing/. See inline on complete.go:255. - Silent error swallowing in the new
listPathCandidates*/completeSubdirectoryInPath*helpers — permission and I/O errors become 'no completions' with no diagnostic signal. See inline on complete.go:357. - Context-cancellation gap in
resolveRelativePathNavigation— doesos.Statwithout checkingctx.Err(). See inline on go.go:421. - Misleading test name —
TestSaveJumpsConfig_DoesNotMutateCallerPathsMappasses only becauseSaveJumpsConfigdoesn't callApplyDefaults; any caller runningApplyDefaultsdirectly would see mutation. See inline on settings_test.go:254. resolved := nav.ResolveConfiguredTarget(cfg, args)runs before toggle / custom-shortcut short-circuits — wasted work on common paths. See inline on go.go:65.
What's Done Well
- The introduction of a single
ResolveConfiguredTargetthat bothcgoand shell completion share is exactly the right abstraction — no parallel implementation of the resolution logic, which would have been easy to regress. NormalizeNavigationNameis a clean primitive and it's applied consistently in the new nav package.BuildPathAliasMappingscorrectly drops ambiguous aliases rather than silently picking one — good defensive design.- Integration tests in
tests/integration/navigation_test.gocover the user-facingcgo design,cgo ai_docs,cgo design/festival_app, andcgo de@festival_siteflows — those exercises the full stack end-to-end. - Build passes, all existing tests pass.
Staff Standard
No. Not in this state. The core design is good and I'd be proud of the overall architecture, but shipping a dead PathsMap field with ~150 lines of plumbing, fmt.Errorf in new code after the project explicitly forbids it, and stale helper functions is not staff-level delivery. Tighten the six 'must-fix' items (PathsMap, fmt.Errorf, dead shortcutKeys, redundant computation, completeAll normalization, error swallowing) and this will be ready.
Rework the configured-target navigation changes in response to the code
review on the parent PR.
- Remove the unused PathsMap field from JumpsConfig plus its plumbing and
tests. Nothing in production code read from it, and the tests gave the
misleading impression that arbitrary user aliases under paths: would
become navigable.
- Replace fmt.Errorf calls in handleRelativePathNavigation /
fuzzyResolveDirectory with camperrors.Wrap{,f} around two sentinel
errors (errNavigationPathNotFound, errNavigationNoMatch) so callers
can distinguish not-found from I/O failure with errors.Is.
- Add ctx.Err() gates at the top of resolveRelativePathNavigation and
before the second os.Stat so the 200ms shell-completion timeout is
respected even on the slow path.
- In ResolveConfiguredTarget, compute BuildCategoryMappings and
BuildPathAliasMappings once per invocation instead of two or three
times; thread the alias map into resolveDrillTarget.
- Rewrite the ResolveConfiguredTarget docstring to match the actual
5-step resolution order (shortcut drill -> shortcut -> slash drill ->
concept -> path alias) and to document that concepts shadow aliases
intentionally.
- Move the ResolveConfiguredTarget call in runGo below the toggle and
custom-path shortcut short-circuits so those hot paths don't pay the
resolution cost.
- Remove the dead shortcutKeys helper in internal/complete.
- Normalize completeAll prefix matching via NormalizeNavigationName on
both sides so queries like "design/" still match the "design" top
level name.
- Add readDirForCompletion helper in internal/complete and have the
path-based completion helpers surface real I/O errors while still
treating a missing directory as "no candidates".
Build, all unit tests, and the affected integration tests pass.
## Summary Follow-up to #208 addressing the review feedback. Targets the PR branch so these fixes land inside that PR's history rather than `main`. Concrete changes: - **Remove `JumpsConfig.PathsMap` dead code.** No production consumer existed; aliases are still derived from `cfg.Shortcuts()` via `BuildPathAliasMappings`. Dropped 50+ lines of plumbing and 100 lines of tests that gave a misleading impression that arbitrary user keys under `paths:` become navigable. - **Replace `fmt.Errorf` with typed sentinels.** New `errNavigationPathNotFound` and `errNavigationNoMatch` in `cmd/camp/navigation/go.go`, wrapped via `camperrors.Wrap{,f}`. Callers can now `errors.Is(err, errNavigationPathNotFound)` to distinguish not-found from I/O failure. - **Add `ctx.Err()` gates** at the top of `resolveRelativePathNavigation` and before the second `os.Stat` in the slash-drill branch. Honors the 200ms shell-completion timeout on the slow path. - **Compute shortcut/alias maps once per `ResolveConfiguredTarget` call.** Previously `BuildCategoryMappings` could run twice and `BuildPathAliasMappings` 2–3 times per invocation in the shell-completion hot path. - **Rewrite the `ResolveConfiguredTarget` docstring** to describe the actual 5-step resolution order and to document the intentional concept-shadows-alias precedence. - **Move `ResolveConfiguredTarget` below the toggle / custom-path shortcut short-circuits** so `cgo t` and `cgo <custom-shortcut>` don't pay the resolution cost. - **Remove dead `shortcutKeys` helper** in `internal/complete/complete.go`. - **Fix normalization inconsistency in `completeAll`.** Both sides now go through `NormalizeNavigationName`, so `cgo design/<TAB>` still proposes the `design` top-level alias. - **Surface real I/O errors from path-based completion helpers.** New `readDirForCompletion` helper swallows `os.IsNotExist` but bubbles permission / bad-symlink failures up instead of degrading to a silent "no candidates". ## Diffstat ``` cmd/camp/navigation/go.go | 29 +++++++++++---- internal/complete/complete.go | 51 +++++++++++++++----------- internal/config/settings.go | 45 ----------------------- internal/config/settings_test.go | 69 +---------------------------------- internal/nav/configured_navigation.go | 55 +++++++++++++++++----------- 5 files changed, 86 insertions(+), 163 deletions(-) ``` Net -77 lines. ## Test plan - [x] `just build quick` — green - [x] `go vet ./...` — clean - [x] `go test ./...` — all packages pass, including `./internal/nav/...`, `./internal/complete/...`, `./internal/config/...`, `./cmd/camp/navigation/...` - [x] `golangci-lint` on the touched packages — no new issues introduced (only pre-existing findings remain) - [ ] Reviewer: manually exercise `cgo design`, `cgo ai_docs`, `cgo design/festival_app`, `cgo de@festival_site` against a real campaign to confirm end-to-end behavior ## Notes for the reviewer - Kept the existing `TestLoadJumpsConfig_AppliesDefaults` as a single renamed test covering the useful invariants (defaulting works, legacy intents path is normalized). - The docstring on `ResolveConfiguredTarget` is now the canonical description of resolution order — if we add another layer, update the docstring at the same time. - The new sentinel errors deliberately stay unexported; scripted callers that care about the difference should match via the package's `camperrors.Is{NotFound,...}` helpers in a future change, or we export the sentinels if a cross-package consumer shows up.
Summary
cgo docs,cgo ai_docs,cgo festivals, etc. now work alongside the shortcut keys (d,ai,f)CampaignPaths.AsMap()to expose path concept names as a mapBuildCategoryMappingsto accept optionalCampaignPathsand merge concept-name mappings alongside shortcut-key mappings (shortcuts win on collision)Root cause
BuildCategoryMappingsonly iterated over theshortcutssection ofjumps.yaml, which maps abbreviation keys (d,ai,f) to categories. Thepathssection defines full concept names (docs,ai_docs,festivals) but was never used for navigation resolution. Socgo docsfell through to fuzzy search with no category and failed.Test plan
go build ./...passesgo test ./...passes (all packages)TestBuildCategoryMappings_WithPathscovers concept name resolution