refactor: index-hook framework — eliminate per-pass CodeGraph mutations#119
refactor: index-hook framework — eliminate per-pass CodeGraph mutations#119andreinknv wants to merge 2 commits into
Conversation
Today every PR adding a derived-signal pass (centrality, churn,
issue-history, config-refs, sql-refs, cochange) edits the same
3 spots in src/index.ts:
1. New imports at the top
2. New private method on `CodeGraph` (e.g. runDerivedSignals,
runIssueHistoryPass, runConfigRefsPass, runSqlRefsPass)
3. New call site in `indexAll` AFTER resolution
4. New call site in `sync` AFTER resolution
5 PRs collide on every one of those.
After this refactor:
Adding a new derived-signal pass:
1. Create `src/index-hooks/<name>.ts` exporting a
`HOOK: IndexHook` constant with `afterIndexAll` and/or
`afterSync` methods.
2. Add one import + one entry to
`src/index-hooks/registry.ts`.
`CodeGraph.indexAll` and `sync` invoke the hook runner once;
adding a new pass touches only the hook file + the registry.
Zero changes to CodeGraph itself.
## What's new
- **src/index-hooks/types.ts** — `IndexHook` interface
(`afterIndexAll`, `afterSync`, both optional), `IndexHookContext`
(projectRoot + config + queries + db), and
`IndexHookOutcome` for diagnostic reporting.
- **src/index-hooks/registry.ts** — static-import list of every
registered hook (empty on main today; PRs adding hooks fill it
in), plus the `runAfterIndexAll` / `runAfterSync` runners that
iterate hooks and catch errors so one broken hook never fails
indexing.
- **src/index.ts** — `indexAll` calls `runAfterIndexAll(ctx)`
after resolution. `sync` calls `runAfterSync(ctx, result)`
after resolution. New private `buildHookContext()` helper
exposes a stable read-only context.
- **__tests__/index-hooks.test.ts** — 6 tests covering empty
registry, runner shape, and the `afterIndexAll` / `afterSync`
contracts.
## Why ship the framework on main with zero registered hooks?
The only consumers of this framework today are 5 unmerged PRs
(colbymchenry#105 cochange + my colbymchenry#112-colbymchenry#115). Landing the framework now lets
each of those PRs rebase to a 2-line change instead of 8-10
lines mutating CodeGraph adjacent-line. Without this, all 5 PRs
collide on the same indexAll/sync call sites.
The framework adds zero behavior on main (no registered hooks =
no-op runner). 380→386 tests confirm no regression.
## Affected open PRs
| PR | Today | After this lands |
|---|---|---|
| colbymchenry#105 cochange | runDerivedSignals helper + 2 call sites | 1 hook file in src/index-hooks/ + 2 lines in registry.ts |
| colbymchenry#112 centrality+churn | same shape | same shape |
| colbymchenry#113 issue-history | same shape | same shape |
| colbymchenry#114 config-refs | same shape | same shape |
| colbymchenry#115 sql-refs | same shape | same shape |
Each goes from "edit CodeGraph in 4 spots" to "drop a hook file."
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Part of a coordinated 4-PR refactor that unblocks the open PR backlog. See #120 for the full merge-order guide explaining how this PR fits into the picture and how each open language/feature PR rebases onto the new pattern. |
Maintainer review checklist (5-min path)This refactor is purely additive on 1. The conflict surface this eliminatesToday, every PR adding a derived-signal pass (centrality, churn, issue-history, config-refs, sql-refs, cochange) edits the same 3 spots in
Five PRs (#105, #112, #113, #114, #115) collide on every one of those adjacent-line edits. After this lands, each PR drops to one new hook file + 2 lines in 2. What changed structurally
3. What stays exactly the same — verify in seconds
4. Why ship the framework with zero registered hooks?The only consumers today are 5 unmerged PRs. Landing the framework first lets each one rebase to a 2-line registry edit instead of an 8-10-line 5. Invariants now enforced by tests (
|
…done heartbeat A hook awaiting a promise that never resolves (e.g. a fetch with no abort, mis-handled worker IPC) used to hang the whole indexAll/sync forever — the runner had no per-hook budget and one stuck await would freeze every later hook plus the post-hook maintenance + caller. Wraps each hook in Promise.race against a 5-minute budget, surfaces the timeout in the IndexHookOutcome.error field, and adds start / done log lines so a stuck hook is identifiable without sampling the process. Hook failures are now logWarn rather than logDebug so they aren't silent without CODEGRAPH_DEBUG=1. Promise.race only unwinds *async* hangs — fully synchronous code still blocks the event loop. That's acceptable: hooks doing heavy work shell out via execFileSync with their own per-call timeout. Adds a regression test covering the racing primitive on a never- resolving promise.
|
Closing — same reasoning as #117. Framework refactor whose only consumers are stacked PRs from the same flood (#105/#112/#113/#114/#115), most of which I've been declining on independent grounds. Codegraph has zero derived-signal passes today, so this is solving a problem that doesn't exist on main. If I decide to add such passes later, I'd rather design the dispatch myself. Thanks for the work. |
refactor: index-hook framework — eliminate per-pass CodeGraph mutations
Today every PR adding a derived-signal pass (centrality, churn,
issue-history, config-refs, sql-refs, cochange) edits the same
3 spots in src/index.ts:
CodeGraph(e.g. runDerivedSignals,runIssueHistoryPass, runConfigRefsPass, runSqlRefsPass)
indexAllAFTER resolutionsyncAFTER resolution5 PRs collide on every one of those.
After this refactor:
Adding a new derived-signal pass:
src/index-hooks/<name>.tsexporting aHOOK: IndexHookconstant withafterIndexAlland/orafterSyncmethods.src/index-hooks/registry.ts.CodeGraph.indexAllandsyncinvoke the hook runner once;adding a new pass touches only the hook file + the registry.
Zero changes to CodeGraph itself.
What's new
IndexHookinterface(
afterIndexAll,afterSync, both optional),IndexHookContext(projectRoot + config + queries + db), and
IndexHookOutcomefor diagnostic reporting.registered hook (empty on main today; PRs adding hooks fill it
in), plus the
runAfterIndexAll/runAfterSyncrunners thatiterate hooks and catch errors so one broken hook never fails
indexing.
indexAllcallsrunAfterIndexAll(ctx)after resolution.
synccallsrunAfterSync(ctx, result)after resolution. New private
buildHookContext()helperexposes a stable read-only context.
registry, runner shape, and the
afterIndexAll/afterSynccontracts.
Why ship the framework on main with zero registered hooks?
The only consumers of this framework today are 5 unmerged PRs
(#105 cochange + my #112-#115). Landing the framework now lets
each of those PRs rebase to a 2-line change instead of 8-10
lines mutating CodeGraph adjacent-line. Without this, all 5 PRs
collide on the same indexAll/sync call sites.
The framework adds zero behavior on main (no registered hooks =
no-op runner). 380→386 tests confirm no regression.
Affected open PRs
Each goes from "edit CodeGraph in 4 spots" to "drop a hook file."
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com