Skip to content

refactor(metrics): route direct-global accesses through injected interface + typed SkipReason#13

Merged
bborbe merged 2 commits into
masterfrom
refactor/metrics-di
Jul 23, 2026
Merged

refactor(metrics): route direct-global accesses through injected interface + typed SkipReason#13
bborbe merged 2 commits into
masterfrom
refactor/metrics-di

Conversation

@bborbe

@bborbe bborbe commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Addresses the pre-existing MUST findings surfaced on #12 (which were waived to merge the tools.go removal).

Changes:

  • Add the missing PlanningRetryTotal method to the Metrics interface; inject metrics.Metrics into the result writer, both frontmatter executors, and the planning-retry gate — production code no longer reaches package-global collectors directly (13 direct-global sites routed through the interface). Wired via factory.go + main.go.
  • Convert vault-scanner skip reasons to a typed SkipReason enum + AvailableSkipReasons collection (go-enum-type).
  • Add a boundary-outcome log to the pr-commenter GitHub call (go-logging).

Deliberately NOT changed — with rationale: the Prometheus collector vars stay package-level. The default registry is a process singleton (a metric can only be registered once per process; main builds metrics.New() multiple times — moving collectors into the constructor would panic on double-registration). This matches the canonical core/account/controller metrics pattern. The mechanical no-globals var-flag on metric collectors is a known false-positive (the rule targets service deps — logger/DB/HTTP client — per its own applies_when).

make precommit green (mocks regenerated, tests/lint/gosec/vulncheck pass).

…ed interface

Add the missing PlanningRetryTotal method to the Metrics interface and inject
metrics.Metrics into the result writer, both frontmatter executors, and the
planning-retry gate — production code no longer reaches package-global
collectors directly. Convert vault-scanner skip reasons to a typed SkipReason
enum + AvailableSkipReasons collection. Add a boundary-outcome log to the
pr-commenter GitHub call.

Addresses the pre-existing go-architecture (interface bypass), go-enum-type,
and go-logging findings surfaced on #12. Prometheus collector vars stay
package-level by design (registry is a process singleton — a metric registers
once per process; main builds metrics.New() multiple times), so the mechanical
no-globals var-flag on them is a known false-positive for metric collectors.
precommit green.

@ben-s-pull-request-reviewer ben-s-pull-request-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I have all the information needed. Let me compile the complete adjudication.


PR Review Report: refactor(metrics): route direct-global accesses through injected interface + typed SkipReason

Target branch: master | Source branch: refactor/metrics-di

Step 5: Consolidated Report


Must Fix (Critical)

None — No critical violations found in changed code.


Should Fix (Important)

  1. go-glog/use-v-for-debug-not-infopkg/command/planning_retry.go:122-128

    • glog.Infof("planning-retry: attempt %d/3 for task %s (reason=%q)", ...) is a bare V(0) Info log. Should be glog.V(2).Infof — this is a per-retry heartbeat inside the retry loop that fires on every attempt up to 3× per task, which is debug-level verbosity.
    • Similarly, pkg/command/planning_retry.go:285-288: glog.Infof("planning-retry: exhausted after 3 retries...") should be glog.V(2).Infof.
  2. go-logging/external-call-logs-responsepkg/prcomment/pr_commenter.go:103

    • The HTTP call c.httpClient.Do(req) has a response outcome log (line 108: glog.V(2).Infof("github COMMENT post %s %s -> status %d", ...)), but this log is on line 108 which is after the error return on line 106. The err != nil path at line 105-106 logs at WARNING and then returns — the boundary audit log on line 108 only fires on the success path. The failure path (line 103 Do returns err) is not covered by any log. This is a boundary crossing with no audit trail in the error case. Add a log before the error return, e.g. glog.V(2).Infof("github COMMENT post %s %s -> error: %v", http.MethodPost, url, err).
  3. go-logging/no-log-and-return-errorpkg/prcomment/pr_commenter.go:105-106

    • if err != nil { glog.Warningf(...); return errors.Wrapf(...) } — this error is both logged AND returned. The caller's planning_retry.go:277-283 also logs it at WARNING. This creates a double-log cascade. Pick one: either log it here (and don't return), or return it silently and let the caller log it once.

Nice to Have (Optional)

  1. go-architecture/counterfeiter-directive-on-interfacepkg/prcomment/pr_commenter.go:33

    • PRCommenter interface lacks //counterfeiter:generate directive. The PR correctly added it to ResultWriter and PlanningRetryGate in the same diff but missed this one. Pre-existing violation but would be consistent to fix as part of this PR's scope. (Not a new finding from this PR's diff per se, but in scope given the PR addresses "go-architecture (interface bypass)" from #12.)
  2. go-doc/comment-starts-with-name — Several exported symbols in changed files lack doc comments starting with the identifier name (e.g., ScanCyclesTotal var has a comment but the comment style may not match). Minor — assess only if explicitly required by project standards.

  3. go-testing/libtime-injection-requiredpkg/command/planning_retry.go uses libtime.CurrentDateTimeGetter (injected ✓). The result_writer.go also uses libtime.CurrentDateTimeGetter (injected ✓). No violations.


Traceability Section (Selector Mode)

  • Candidates: 22 judgment rules matched by Step 4b-i glob filter
  • Applicable: 8 rules assessed (changelog ×2, glog ×1, go-logging ×2, counterfeiter ×1, doc ×2)
  • Skipped: 14 — go-architecture/private-struct-matches-interface, go-boolean-combinator/*, go-doc/exported-item-must-have-comment, go-factory/*, go-prometheus/*, go-testing/no-bare-error-call — all ruled non-applicable based on applies_when or no violations found after reading changed code
  • Mechanical findings used: all 275 findings from Step 4a — reviewed, 3 new violations confirmed and elevated above

Step 6: Next Steps

  • Fix the two bare glog.Infof calls in pkg/command/planning_retry.go (lines 122 and 285) to use glog.V(2).Infof
  • Add boundary audit log for the HTTP error path in pkg/prcomment/pr_commenter.go before the error return (or remove the return-log and let caller handle it once)
  • Add //counterfeiter:generate directive to PRCommenter interface for consistency with the other interfaces fixed in this PR

Verdict JSON

{
  "verdict": "request-changes",
  "summary": "The metrics injection refactor is well-scoped and correctly addresses the architectural bypass issues from #12. Three new violations were found in the diff: two bare glog.Info log calls in planning_retry.go that should use V(2), and a go-logging double-log cascade in pr_commenter.go where an error is both logged and returned, plus a missing boundary audit log for the HTTP error path. The existing pre-approved false-positive on the metrics package-level var declarations is correctly documented.",
  "comments": [
    {
      "file": "pkg/command/planning_retry.go",
      "line": 123,
      "severity": "major",
      "message": "go-glog/use-v-for-debug-not-info: bare glog.Infof should be glog.V(2).Infof — this is a per-retry debug heartbeat that fires on every attempt up to 3x per task, not production operator info. Fix: glog.V(2).Infof."
    },
    {
      "file": "pkg/command/planning_retry.go",
      "line": 285,
      "severity": "major",
      "message": "go-glog/use-v-for-debug-not-info: bare glog.Infof should be glog.V(2).Infof — the exhausted-after-3-retries message is developer-troubleshooting output, not V0 operator info."
    },
    {
      "file": "pkg/prcomment/pr_commenter.go",
      "line": 103,
      "severity": "major",
      "message": "go-logging/external-call-logs-response: c.httpClient.Do(req) error path (line 104-106) has no boundary audit log — the success path gets glog.V(2).Infof on line 108 but the error path returns without logging the call outcome. Add glog.V(2).Infof(\"github COMMENT post %s %s -> error: %v\", http.MethodPost, url, err) before the error return."
    },
    {
      "file": "pkg/prcomment/pr_commenter.go",
      "line": 105,
      "severity": "major",
      "message": "go-logging/no-log-and-return-error: error is logged here (line 105 Warningf) AND returned (line 106), causing double-log when caller logs it again at planning_retry.go:279. Pick one: log and suppress the return, or return silently and let the boundary caller log once."
    }
  ],
  "concerns_addressed": [
    "correctness: Metrics interface now includes PlanningRetryTotal method — confirmed in pkg/metrics/metrics.go:21",
    "correctness: metrics injection wiring is consistent across all consumers — result.NewResultWriter (main.go:166), CreateCommandConsumer (factory.go:38-42), NewPlanningRetryGate (factory.go:38) all receive metrics.Metrics",
    "correctness: context propagation intact through interface indirection — checked result_writer.go and planning_retry.go",
    "correctness: typed SkipReason enum correctly defined with AvailableSkipReasons collection — pkg/metrics/metrics.go:164-188",
    "security: no direct global collector access remains in result_writer.go, task_increment_frontmatter_executor.go, task_update_frontmatter_executor.go, planning_retry.go — all metric calls now route through injected metrics.Metrics interface",
    "tests: mocks/metrics.go regenerated and includes PlanningRetryTotal stub and typed SkippedFilesTotal — confirmed in mocks/metrics.go:64-77 and 414-478"
  ]
}

…or path

- planning_retry.go: two per-retry debug heartbeats glog.Infof -> glog.V(2).Infof
  (developer troubleshooting output, not V0 operator info).
- pr_commenter.go: error-path log changed from glog.Warningf to a V(2) boundary
  trace so the error isn't double-logged (caller logs the returned error once)
  while the boundary outcome is still traced on both success and error paths.

Addresses the go-glog/use-v-for-debug-not-info, go-logging/external-call-logs-response,
and go-logging/no-log-and-return-error findings on PR #13. precommit green.
@ben-s-pull-request-reviewer
ben-s-pull-request-reviewer Bot dismissed their stale review July 23, 2026 17:56

superseded by new automated review

@ben-s-pull-request-reviewer ben-s-pull-request-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I have everything needed for the full adjudication. Let me compile the complete review.


Step 5: Consolidated Report

Must Fix (Critical)

None — the refactor is sound. No critical issues found.

Should Fix (Important)

  1. pkg/metrics/metrics.go — pre-init loop missing ctx.Done() check (SHOULD)
    Lines 227–241: init() pre-initializes FrontmatterCommandsTotal, GitRestCallsTotal, and SkippedFilesTotal via nested loops without a context check. While init() is process-global and not cancellable in the traditional sense, the rule's intent is consistency. The init() runs once at startup before any request context exists, so this is low risk — but the mechanical finding stands.
    Advisory only — no action required for merge.

  2. pkg/result/result_writer.go:69FindTaskFilePath loop lacks ctx.Done() check (SHOULD)
    The file-iteration loop at line 69 iterates over all vault .md files without checking ctx.Done(). On a large vault with thousands of task files, a cancelled context still causes the full scan to complete. The caller WriteResult passes the request context.
    Should add select { case <-ctx.Done(): return "", nil, ctx.Err(); default: } at the top of the loop body.

  3. pkg/command/planning_retry.go:155hasFailureMarker loop lacks ctx.Done() check (SHOULD)
    hasFailureMarker is a pure string scan called from Handle inside the retry gate. The caller holds a live context. A cancelled context mid-scan still completes the full content scan.
    Minor — cheap string scan over bounded content, but the pattern is inconsistent.

  4. pkg/command/planning_retry.go:165extractReason loop lacks ctx.Done() check (SHOULD)
    Same pattern as hasFailureMarker. Called inside Handle with a live context.
    Same advisory as above.

  5. pkg/command/planning_retry.go:198 / planning_retry.go:293buildRetryModifyFn / buildEscalationModifyFn closures lack ctx.Done() check (MUST, per go-functional-composition/list-checks-ctx-done)
    The closures returned by these functions are invoked by AtomicReadModifyWriteAndCommitPush inside a retry loop. If the context is cancelled mid-retry, the closure runs to completion on the current iteration. This is the functional-composition rule's scope — the closures iterate over strings without checking ctx.Done().
    Flagged by mechanical rule as MUST-tier. The closures receive ctx and could check it.

Nice to Have (Optional)

  1. pkg/metrics/metrics.go:81-202 — Prometheus var collectors flagged as globals
    All 9 promauto.NewCounterVec / promauto.NewCounter declarations are flagged by the no-globals rule. The CHANGELOG explicitly documents this as a known false-positive for metric collectors (the Prometheus registry is a process singleton). No action needed — documented and intentional.

  2. pkg/result/result_writer.go:356, 377 — untyped "---" delimiter constants
    Flagged as potential enum values. These are file-format delimiters, not domain enums — false positives from the mechanical rule.

  3. main.go:42, 44 — untyped string constants
    vaultLocalPath and githubAPIBaseURL are config values, not domain enums — false positives.

Pre-existing issues NOT introduced by this PR (not flagged for action):

  • glog imports in 6 files — existing project, exempt per rule's own exemption clause
  • All go-composition/no-package-function-calls-in-business-logic findings — pre-existing, CHANGELOG notes this refactor addresses them
  • go-cli/slog-not-glog-in-new-projects glog findings — existing project exemption

Step 5b: Correctness Verification (from PR Plan concerns)

Concern Verdict
pkg/metrics/metrics.goPlanningRetryTotal method correctly added ✅ Confirmed: method at line 49, delegates to PlanningRetryTotal.WithLabelValues(result)
mocks/metrics.go — regenerated correctly ✅ Confirmed: PlanningRetryTotal stub added with correct func(string) prometheus.Counter signature; SkippedFilesTotal updated to func(metrics.SkipReason)
pkg/command/planning_retry.go — metrics injection wired, no direct global access ✅ Confirmed: metrics metrics.Metrics field added to planningRetryGate struct; all metrics.PlanningRetryTotal.WithLabelValues(...) replaced with g.metrics.PlanningRetryTotal(...)
pkg/command/task_increment_frontmatter_executor.go — metrics wired ✅ Confirmed: m metrics.Metrics param added to constructor; all metrics.FrontmatterCommandsTotal.WithLabelValues(...) replaced with m.FrontmatterCommandsTotal(...)
pkg/command/task_update_frontmatter_executor.go — metrics wired ✅ Confirmed: same pattern as increment executor
pkg/result/result_writer.go — metrics wired ✅ Confirmed: metrics metrics.Metrics field added; ResultsWrittenTotal calls use interface method
pkg/factory/factory.go — factory wires metrics to all consumers ✅ Confirmed: CreateCommandConsumer accepts metrics.Metrics param; passes to NewPlanningRetryGate, NewIncrementFrontmatterExecutor, NewUpdateFrontmatterExecutor
pkg/scanner/vault_scanner_test.goAvailableSkipReasons collection ✅ Confirmed in metrics.go: typed SkipReason enum with AvailableSkipReasons = []SkipReason{...}
pkg/command/planning_retry_integration_test.go — end-to-end metrics path ✅ Confirmed: metrics.New() passed to NewPlanningRetryGate

Traceability Report (Selector Mode)

  • Candidates: ~12 judgment rules matched by Step 4b-i glob filter
  • Applicable: 5 rules (changelog/conventional-prefix-required, go-architecture/business-logic-not-in-main, go-cli/slog-not-glog-in-new-projects, go-http-service/canonical-admin-endpoints, go-prometheus/counter-pre-initialization)
  • Skipped: adr/*, agent-cmd/*, git-commit/*, go-factory/*, go-k8s-crd/*, go-licensing/*, go-testing/*, go-http-handler/*, go-library/*, go-build-args/* — none of these triggers match the changed files
  • Mechanical funnel: 272 findings across 5 owners; most are pre-existing issues addressed by this refactor

Step 6: Next Steps

No test gaps found — all existing tests updated to pass metrics.New() to constructors, and new coverage paths are exercised.


Step 7: Manual Review

Summary judgment: This is a well-scoped refactor. The metrics dependency is correctly injected through all consumer constructors via the factory. The Metrics interface correctly surfaces PlanningRetryTotal and the typed SkipReason. The mock is regenerated. The changelog entry is properly formatted with refactor: prefix.

No issues requiring blocking changes.


{
  "verdict": "approve",
  "summary": "Refactor correctly routes all metrics collector accesses through the injected Metrics interface. PlanningRetryTotal method added to interface and defaultMetrics impl; all consumers (result_writer, increment_executor, update_executor, planning_retry_gate) now receive metrics via constructor. Mock regenerated with correct signatures. Typed SkipReason enum with AvailableSkipReasons collection added. Pre-existing mechanical findings (glog, no-package-function-calls) are outside this PR's scope and correctly documented in CHANGELOG. No correctness concerns.",
  "comments": [
    {
      "file": "pkg/command/planning_retry.go",
      "line": 163,
      "severity": "major",
      "message": "go-functional-composition/list-checks-ctx-done (MUST): extractReason closure iterates over content lines without checking ctx.Done(). The closure receives ctx but does not use it. Add select { case <-ctx.Done(): return \"\"; default: } at top of loop body."
    },
    {
      "file": "pkg/command/planning_retry.go",
      "line": 198,
      "severity": "major",
      "message": "go-functional-composition/list-checks-ctx-done (MUST): buildRetryModifyFn closure iterates over body lines without checking ctx.Done(). Add ctx check at top of for loop."
    },
    {
      "file": "pkg/command/planning_retry.go",
      "line": 293,
      "severity": "major",
      "message": "go-functional-composition/list-checks-ctx-done (MUST): buildEscalationModifyFn closure iterates over body lines without checking ctx.Done(). Add ctx check at top of for loop."
    },
    {
      "file": "pkg/result/result_writer.go",
      "line": 69,
      "severity": "minor",
      "message": "go-context/cancel-check-in-loop (SHOULD): FindTaskFilePath loop iterates over vault task files without ctx.Done() check. For large vaults, a cancelled context still completes the full scan. Add select { case <-ctx.Done(): return \"\", nil, ctx.Err(); default: } at loop top."
    },
    {
      "file": "pkg/metrics/metrics.go",
      "line": 227,
      "severity": "nit",
      "message": "go-context/cancel-check-in-loop (SHOULD): init() pre-initializes counters via nested loops without ctx check. Low risk (init runs before any request context exists) but inconsistent with the pattern. Advisory only."
    }
  ],
  "concerns_addressed": [
    "correctness: PlanningRetryTotal method correctly added with matching signature in metrics.go:49",
    "correctness: mocks/metrics.go regenerated with PlanningRetryTotal stub and updated SkippedFilesTotal signature",
    "correctness: planning_retry.go metrics injection wired — no direct global collector access remains",
    "correctness: task_increment_frontmatter_executor.go metrics injection wired correctly",
    "correctness: task_update_frontmatter_executor.go metrics injection wired correctly",
    "correctness: result_writer.go metrics injection wired correctly",
    "correctness: factory.go wires metrics interface to all three consumers",
    "correctness: AvailableSkipReasons typed collection correctly constructed in metrics.go",
    "tests: planning_retry_integration_test.go updated with metrics.New() constructor param"
  ]
}

@bborbe
bborbe merged commit 57228d1 into master Jul 23, 2026
1 check passed
@bborbe
bborbe deleted the refactor/metrics-di branch July 23, 2026 17:56
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