Skip to content

Commit 470599a

Browse files
committed
Merge branch 'worktree-agent-aa348bf88487d0537' into claude/subagents-deploy-phases-137-141-3d6vm5
# Conflicts: # docs/features.md # docs/parity.md
2 parents 4f91acb + 887c2dc commit 470599a

6 files changed

Lines changed: 142 additions & 5 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitsema": minor
3+
---
4+
5+
`POST /analysis/author` (HTTP API) now supports the full `gitsema author` CLI flag surface: `since`, `detail`, `includeCommits`, `hybrid`, and `bm25Weight` are wired through to the same author-attribution logic the CLI uses, plus `chunks`/`level`/`vss` are accepted for flag-surface parity (no-op, matching the CLI's own behavior for these three). Breaking change: the response shape is now `{ authors, commits? }` instead of a bare array, to carry `includeCommits` results.

docs/PLAN.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5581,7 +5581,21 @@ Update `docs/parity.md`, `docs/features.md`, `README.md`.
55815581
`src/core/search/authorSearch.ts` (or wherever author attribution lives),
55825582
`docs/parity.md`, test files.
55835583

5584-
**Status:** not started.
5584+
**Status:** ✅ complete. `AuthorBodySchema` in `src/server/routes/analysis.ts`
5585+
gained `since`, `detail`, `includeCommits`, `hybrid`, `bm25Weight` (all
5586+
threaded through to `computeAuthorContributions`/`hybridSearch`/
5587+
`searchCommits`, mirroring `src/cli/commands/author.ts` exactly) plus
5588+
`chunks`/`level`/`vss` for flag-surface parity — these three are accepted
5589+
but deliberately not wired to anything, matching the CLI's own dead-flag
5590+
behavior (`computeAuthorContributions` is blob-level only; the CLI's
5591+
`--vss` on `author` is itself just a warn-and-ignore no-op). Model-override
5592+
triplet (`model`/`textModel`/`codeModel`) intentionally excluded — tracked
5593+
separately in Phase 140. **Deviation from spec:** response shape changed
5594+
from a bare `AuthorContribution[]` array to `{ authors, commits? }` (the
5595+
`commits` key populated only when `includeCommits` is set) — a breaking
5596+
change, taken deliberately per `docs/parity.md` §4's "parity over API
5597+
response stability" rule, since a bare array had no room for
5598+
`includeCommits`'s second result set.
55855599

55865600
---
55875601

docs/features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ Start with `gitsema tools serve [--port n] [--key token] [--ui]`.
253253
| `GET /api/v1/remote/jobs/metrics`, `GET /api/v1/remote/jobs/:id/progress` | Job progress |
254254
| `POST /api/v1/analysis/clusters` | Clustering — accepts `{model, textModel, codeModel}` overrides for CLI/HTTP flag parity (Phase 140), though `computeClusters()` doesn't filter by model so behavior is unchanged today |
255255
| `POST /api/v1/analysis/change-points` | Change-point detection — accepts `{model, textModel, codeModel}` embedding overrides (Phase 140) |
256-
| `POST /api/v1/analysis/author` | Author attribution — accepts `{model, textModel, codeModel}` embedding overrides (Phase 140) |
256+
| `POST /api/v1/analysis/author` | Author attribution — full CLI flag parity (Phase 141): `since`, `detail`, `includeCommits`, `hybrid`, `bm25Weight` all wired through, plus `{model, textModel, codeModel}` embedding overrides (Phase 140); response is `{ authors, commits? }` |
257257
| `POST /api/v1/analysis/impact` | Impact analysis — accepts `{model, textModel, codeModel}` embedding overrides (Phase 140) |
258258
| `POST /api/v1/analysis/semantic-diff` | Semantic diff — accepts `{model, textModel, codeModel}` embedding overrides (Phase 140) |
259259
| `POST /api/v1/analysis/semantic-blame` | Semantic blame — accepts `{model, textModel, codeModel}` embedding overrides (Phase 140) |

docs/parity.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,11 @@ This table shows less common flags used by specific commands or command groups.
336336
| `--include-content` | `evolution`, `concept-evolution` | bool | false | Add stored file text in JSON output |
337337
| `--include-commits` | `first-seen` | bool | false | Also search commit messages |
338338
| `--alerts` | `file-evolution` | [int] | `5` | Top-N largest semantic jumps with author/commit-URL enrichment; `true`/flag-only → default 5. Also `alerts: <n>` (required, no implicit default) on `POST /evolution/file` since Phase 139 |
339+
| `--since` | `author` | date || Only consider contributions since this date (HTTP: also accepted by `POST /analysis/author`, Phase 141) |
340+
| `--detail` | `author` | bool | false | Include per-blob contribution detail (HTTP: also accepted by `POST /analysis/author`, Phase 141) |
341+
| `--hybrid` | `author` | bool | false | Use hybrid (vector + BM25) candidate selection (HTTP: also accepted by `POST /analysis/author`, Phase 141) |
342+
| `--bm25-weight` | `author` | float | 0.3 | BM25 weight when `--hybrid` is set (HTTP: also accepted by `POST /analysis/author`, Phase 141) |
343+
| `--chunks` / `--level` / `--vss` | `author` | bool/enum/bool | false/—/false | Declared on the CLI but not wired to anything in `computeAuthorContributions` (blob-level only); `--vss` prints a warning and is ignored. HTTP's `POST /analysis/author` accepts all three for flag-surface parity with the same no-op behavior (Phase 141) — not a gap, a documented CLI limitation mirrored intentionally. |
339344

340345
### 2.3 Flag Coherence Issues
341346

@@ -569,7 +574,15 @@ this section's prior open-ended bullets into concrete, numbered
569574
- **Phase 140:** systemic `--model`/`--text-model`/`--code-model` gap
570575
across `analysis.ts` HTTP routes (one shared fix, not 8 per-route ones).
571576
- **Phase 141:** `author` HTTP route full parity (largest single-command
572-
gap found).
577+
gap found). ✅ done — `POST /analysis/author` now accepts `since`,
578+
`detail`, `includeCommits`, `hybrid`, `bm25Weight` (all wired to
579+
`computeAuthorContributions`/`hybridSearch`/`searchCommits`, mirroring the
580+
CLI `author` command exactly) plus `chunks`/`level`/`vss` for flag-surface
581+
parity (accepted, no-op — matching the CLI's own dead-flag behavior for
582+
these three). Response shape changed from a bare array to `{ authors,
583+
commits? }` (breaking change, per §4's parity-over-stability rule) to
584+
carry `includeCommits` results. Model-override triplet intentionally
585+
excluded — tracked separately in Phase 140.
573586
- **Phase 142:** `workflow` HTTP route parity — 3/8 templates exposed
574587
today, all 8 on CLI.
575588
- **Phase 143:** analysis-route small-fixes bundle (merge-audit,

src/server/routes/analysis.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import type { Embedding } from '../../core/models/types.js'
2323
import { computeClusters, getBlobHashesOnBranch } from '../../core/search/clustering/clustering.js'
2424
import { computeConceptChangePoints } from '../../core/search/temporal/changePoints.js'
2525
import { computeAuthorContributions } from '../../core/search/authorSearch.js'
26+
import { hybridSearch } from '../../core/search/analysis/hybridSearch.js'
27+
import { searchCommits } from '../../core/search/commitSearch.js'
2628
import { computeExperts } from '../../core/search/experts.js'
2729
import { computeImpact } from '../../core/search/impact.js'
2830
import { parseDateArg } from '../../core/search/temporal/timeSearch.js'
@@ -69,6 +71,19 @@ const AuthorBodySchema = z.object({
6971
topK: z.number().int().positive().optional().default(50),
7072
topAuthors: z.number().int().positive().optional().default(10),
7173
branch: z.string().optional(),
74+
since: z.string().optional(),
75+
detail: z.boolean().optional().default(false),
76+
includeCommits: z.boolean().optional().default(false),
77+
hybrid: z.boolean().optional().default(false),
78+
bm25Weight: z.number().min(0).max(1).optional().default(0.3),
79+
// `chunks`/`level`/`vss` are accepted for CLI flag-surface parity but are
80+
// not wired to anything — `computeAuthorContributions` is blob-level only,
81+
// and the CLI's `author` command itself treats these three the same way
82+
// (declared flags with no effect on results; `--vss` just prints a
83+
// warning). See docs/parity.md for the audit note.
84+
chunks: z.boolean().optional().default(false),
85+
level: z.enum(['file', 'chunk', 'symbol']).optional(),
86+
vss: z.boolean().optional().default(false),
7287
})
7388

7489
const ImpactBodySchema = z.object({
@@ -168,6 +183,15 @@ export function analysisRouter(deps: AnalysisRouterDeps): Router {
168183
return
169184
}
170185
const opts = parsed.data
186+
187+
let since: number | undefined
188+
try {
189+
if (opts.since) since = parseDateArg(opts.since)
190+
} catch (err) {
191+
res.status(400).json({ error: `Date parse error: ${err instanceof Error ? err.message : String(err)}` })
192+
return
193+
}
194+
171195
let queryEmbedding: Embedding
172196
try {
173197
const provider = resolveRequestProvider(opts, textProvider)
@@ -178,12 +202,35 @@ export function analysisRouter(deps: AnalysisRouterDeps): Router {
178202
return
179203
}
180204
try {
205+
// Mirrors the CLI `author` command (src/cli/commands/author.ts): when
206+
// --hybrid is set, use hybridSearch to get pre-scored candidate blobs
207+
// instead of scoring the whole embeddings table by cosine similarity.
208+
let candidateBlobs: Array<{ blobHash: string; score: number }> | undefined
209+
if (opts.hybrid) {
210+
const hybridResults = await hybridSearch(opts.query, queryEmbedding, {
211+
topK: 50,
212+
bm25Weight: opts.bm25Weight,
213+
branch: opts.branch,
214+
})
215+
candidateBlobs = hybridResults.map((r) => ({ blobHash: r.blobHash, score: r.score }))
216+
}
217+
181218
const contributions = await computeAuthorContributions(queryEmbedding, {
182219
topK: opts.topK,
183220
topAuthors: opts.topAuthors,
184221
branch: opts.branch,
222+
since,
223+
detail: opts.detail,
224+
candidateBlobs,
185225
})
186-
res.json(contributions)
226+
227+
if (opts.includeCommits) {
228+
const commitResults = await searchCommits(queryEmbedding, { topK: opts.topK, model: textProvider.model })
229+
res.json({ authors: contributions, commits: commitResults })
230+
return
231+
}
232+
233+
res.json({ authors: contributions })
187234
} catch (err) {
188235
res.status(500).json({ error: err instanceof Error ? err.message : String(err) })
189236
}

tests/serverRoutes.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -857,12 +857,70 @@ describe('POST /api/v1/analysis/author', () => {
857857
expect(res.status).toBe(400)
858858
})
859859

860-
it('returns 200 with contributions structure (empty DB)', async () => {
860+
it('returns 200 with { authors } structure (empty DB)', async () => {
861861
const res = await request(app)
862862
.post('/api/v1/analysis/author')
863863
.send({ query: 'authentication' })
864864
expect(res.status).toBe(200)
865865
expect(typeof res.body).toBe('object')
866+
expect(Array.isArray(res.body.authors)).toBe(true)
867+
expect(res.body.authors.length).toBe(0)
868+
expect(res.body.commits).toBeUndefined()
869+
})
870+
871+
it('accepts --since (Phase 141) and returns 200 with a valid date', async () => {
872+
const res = await request(app)
873+
.post('/api/v1/analysis/author')
874+
.send({ query: 'authentication', since: '2020-01-01' })
875+
expect(res.status).toBe(200)
876+
expect(Array.isArray(res.body.authors)).toBe(true)
877+
})
878+
879+
it('returns 400 for an unparseable --since value', async () => {
880+
const res = await request(app)
881+
.post('/api/v1/analysis/author')
882+
.send({ query: 'authentication', since: 'not-a-date-at-all-!!' })
883+
expect(res.status).toBe(400)
884+
})
885+
886+
it('accepts --detail (Phase 141) without error', async () => {
887+
const res = await request(app)
888+
.post('/api/v1/analysis/author')
889+
.send({ query: 'authentication', detail: true })
890+
expect(res.status).toBe(200)
891+
expect(Array.isArray(res.body.authors)).toBe(true)
892+
})
893+
894+
it('accepts --hybrid + --bm25-weight (Phase 141) without error', async () => {
895+
const res = await request(app)
896+
.post('/api/v1/analysis/author')
897+
.send({ query: 'authentication', hybrid: true, bm25Weight: 0.5 })
898+
expect(res.status).toBe(200)
899+
expect(Array.isArray(res.body.authors)).toBe(true)
900+
})
901+
902+
it('accepts --include-commits (Phase 141) and returns a commits array', async () => {
903+
const res = await request(app)
904+
.post('/api/v1/analysis/author')
905+
.send({ query: 'authentication', includeCommits: true })
906+
expect(res.status).toBe(200)
907+
expect(Array.isArray(res.body.authors)).toBe(true)
908+
expect(Array.isArray(res.body.commits)).toBe(true)
909+
})
910+
911+
it('accepts chunks/level/vss (Phase 141 flag-surface parity, no-op like the CLI)', async () => {
912+
const res = await request(app)
913+
.post('/api/v1/analysis/author')
914+
.send({ query: 'authentication', chunks: true, level: 'symbol', vss: true })
915+
expect(res.status).toBe(200)
916+
expect(Array.isArray(res.body.authors)).toBe(true)
917+
})
918+
919+
it('returns 400 for an invalid level enum value', async () => {
920+
const res = await request(app)
921+
.post('/api/v1/analysis/author')
922+
.send({ query: 'authentication', level: 'not-a-real-level' })
923+
expect(res.status).toBe(400)
866924
})
867925
})
868926

0 commit comments

Comments
 (0)