Skip to content

Plan C Phase 6: deduplicate instrument.clj fdef registry#94

Merged
krukow merged 1 commit intomainfrom
plan-c/instrument-dedup
Apr 28, 2026
Merged

Plan C Phase 6: deduplicate instrument.clj fdef registry#94
krukow merged 1 commit intomainfrom
plan-c/instrument-dedup

Conversation

@krukow
Copy link
Copy Markdown
Collaborator

@krukow krukow commented Apr 28, 2026

Plan C Phase 6: deduplicate instrument.clj fdef registry

Eliminates the three-parallel-list maintenance burden in
src/github/copilot_sdk/instrument.clj.

Problem

Adding a public API function previously required edits in three places:

  1. The (s/fdef sym ...) declaration in instrument.clj
  2. The symbol list passed to stest/instrument in instrument-all!
  3. The symbol list passed to stest/unstrument in unstrument-all!

This triplication was documented in AGENTS.md as a known weakness.
Drift between the three lists would silently leave an instrumentation gap
for the missing symbol.

Solution

A single private register-fdef! macro records each fdef into a single
private registry; both (un)instrument-all! derive from it.

(def ^:private registered-fdefs (atom #{}))

(defmacro ^:private register-fdef!
  [sym & body]
  ;; Reject unqualified symbols
  (when-not (and (symbol? sym) (namespace sym))
    (throw (ex-info "register-fdef! requires a fully-qualified symbol"
                    {:sym sym})))
  ;; Reject alias-qualified symbols — `s/fdef` would resolve the alias when
  ;; registering the spec, but `'~sym` would record the unresolved alias
  ;; symbol; `stest/instrument` would then silently skip it.
  (when (contains? (ns-aliases *ns*) (symbol (namespace sym)))
    (throw (ex-info "register-fdef! requires a fully-qualified namespace, not an alias"
                    {:sym sym :alias (namespace sym)})))
  `(let [ret# (s/fdef ~sym ~@body)]
     (swap! registered-fdefs conj '~sym)
     ret#))

(defn instrument-all! []
  (stest/instrument (vec @registered-fdefs)))

(defn unstrument-all! []
  (stest/unstrument (vec @registered-fdefs)))

All 98 hand-written fdefs were mechanically migrated from s/fdef to
register-fdef!. The trailing (instrument-all!) call still works because
all 98 register-fdef! forms run (and populate the atom) before it.

Net effect

  • instrument.clj: 689 → 527 lines (~162 lines of pure repetition removed)
  • Workflow: adding a public API fn now requires one edit, not three
  • No behavior change — symbol set passed to stest/instrument is
    identical to the old hard-coded vector
  • AGENTS.md workflow doc updated accordingly

Defense in depth

The macro fail-fast at macroexpansion time:

  • rejects unqualified symbols (would otherwise be silently mis-registered)
  • rejects alias-qualified symbols (e.g. client/foo where client is a
    (:require ... :as client) alias). s/fdef resolves aliases at
    registration time, but '~sym records the unresolved symbol — so
    stest/instrument would receive a symbol that doesn't match any var.
    This was caught by both the rubber-duck critique (pre-impl) and the
    GPT-5.5 round-1 review.

Validation

  • bb test: 188 tests, 605 assertions, 0 failures, 0 errors
  • bb ci:full (on the codegen-pipeline branch where this work was
    initially developed before being moved to its own branch): exit 0
  • Reviewed in parallel by Claude Opus 4.7, GPT-5.5 (extra-high reasoning),
    and GPT-5.3-Codex (extra-high reasoning). Round 1: one Medium finding
    (alias guard) raised by GPT-5.5; fixed. Round 2: clean.

Files changed

  • src/github/copilot_sdk/instrument.clj — registry + macro + 98 form rewrites + simplified (un)instrument-all!
  • .github/copilot-instructions.md — workflow updated from 4 steps to 3 (single edit replaces "add to fdef list AND instrument-all! AND unstrument-all!")
  • CHANGELOG.md — Unreleased entry under "### Changed (instrumentation)"

Plan C progress

This is Phase 6 of Plan C. Phase 1+3+3.5 (codegen pipeline + three-tier
wire/coerce/idiom architecture) is in #93. Phase 7 (mock v3 coverage) and
Phase 5 (RPC fdef codegen — blocked on upstream api.schema.json) remain.

This branch is independent of #93 — it's branched directly from main so
it can land independently.

Replace three parallel symbol lists in `src/github/copilot_sdk/instrument.clj`
with a single registry populated at fdef-declaration time:

* New private `register-fdef!` macro delegates to `s/fdef` and records the
  fully-qualified symbol in a private `registered-fdefs` atom.
* `instrument-all!` and `unstrument-all!` derive their target list from the
  registry — no more parallel symbol lists.
* All 98 hand-written fdefs migrated from `s/fdef` to `register-fdef!`.
* Macro fail-fast at macroexpansion time: rejects unqualified symbols and
  alias-qualified symbols (e.g. `client/foo` where `client` is an
  `(:require ... :as client)` alias). The latter would otherwise silently
  leave an instrumentation gap because `'~sym` records the unresolved alias
  while `s/fdef` registers under the resolved namespace.

Workflow change: adding a public API fn now requires editing exactly one
place (the `register-fdef!` form) instead of three. AGENTS.md updated.

Net diff: instrument.clj 689 → 527 lines (~162 lines removed, no behavior
change). `bb test` passes 188/605 0 failures.

Reviewed in parallel by Claude Opus 4.7, GPT-5.5 (extra-high reasoning),
and GPT-5.3-Codex (extra-high reasoning). Round 1 surfaced one Medium
finding (alias-qualified guard gap, also raised by rubber-duck pre-impl)
which was fixed; round 2 confirmed no remaining issues.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 28, 2026 10:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR removes the duplicated “three parallel lists” maintenance pattern in instrument.clj by introducing a single registry-backed macro for declaring clojure.spec fdefs, and updating docs/changelog accordingly.

Changes:

  • Add a private register-fdef! macro + registered-fdefs registry to record fdef symbols at definition time.
  • Migrate all existing public API fdefs to register-fdef! and simplify (un)instrument-all! to use the registry.
  • Update contributor instructions and add an Unreleased changelog entry documenting the workflow change.
Show a summary per file
File Description
src/github/copilot_sdk/instrument.clj Introduces registry + macro; replaces hard-coded instrument/unstrument symbol vectors with registry-derived set.
CHANGELOG.md Documents the instrumentation workflow change under Unreleased.
.github/copilot-instructions.md Updates the “adding new public functions” workflow to use register-fdef! (single edit).

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0

@krukow krukow marked this pull request as ready for review April 28, 2026 13:11
@krukow krukow merged commit efe971e into main Apr 28, 2026
5 checks passed
@krukow krukow deleted the plan-c/instrument-dedup branch April 28, 2026 13:13
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.

2 participants