Skip to content

Commit 887c2dc

Browse files
committed
Phase 141: author HTTP route full parity
POST /analysis/author now accepts since, detail, includeCommits, hybrid, and bm25Weight, threaded through to the same computeAuthorContributions/ hybridSearch/searchCommits logic the CLI author command uses. Also accepts chunks/level/vss for flag-surface parity (deliberately no-op, matching the CLI's own dead-flag behavior for these three). Model-override triplet stays out of scope (tracked separately in Phase 140). Breaking change: response shape is now { authors, commits? } instead of a bare array, to carry includeCommits results, per parity.md's parity-over- API-stability rule.
1 parent 225b92c commit 887c2dc

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
@@ -5439,7 +5439,21 @@ Update `docs/parity.md`, `docs/features.md`, `README.md`.
54395439
`src/core/search/authorSearch.ts` (or wherever author attribution lives),
54405440
`docs/parity.md`, test files.
54415441

5442-
**Status:** not started.
5442+
**Status:** ✅ complete. `AuthorBodySchema` in `src/server/routes/analysis.ts`
5443+
gained `since`, `detail`, `includeCommits`, `hybrid`, `bm25Weight` (all
5444+
threaded through to `computeAuthorContributions`/`hybridSearch`/
5445+
`searchCommits`, mirroring `src/cli/commands/author.ts` exactly) plus
5446+
`chunks`/`level`/`vss` for flag-surface parity — these three are accepted
5447+
but deliberately not wired to anything, matching the CLI's own dead-flag
5448+
behavior (`computeAuthorContributions` is blob-level only; the CLI's
5449+
`--vss` on `author` is itself just a warn-and-ignore no-op). Model-override
5450+
triplet (`model`/`textModel`/`codeModel`) intentionally excluded — tracked
5451+
separately in Phase 140. **Deviation from spec:** response shape changed
5452+
from a bare `AuthorContribution[]` array to `{ authors, commits? }` (the
5453+
`commits` key populated only when `includeCommits` is set) — a breaking
5454+
change, taken deliberately per `docs/parity.md` §4's "parity over API
5455+
response stability" rule, since a bare array had no room for
5456+
`includeCommits`'s second result set.
54435457

54445458
---
54455459

docs/features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ Start with `gitsema tools serve [--port n] [--key token] [--ui]`.
250250
| `GET /api/v1/remote/jobs/metrics`, `GET /api/v1/remote/jobs/:id/progress` | Job progress |
251251
| `POST /api/v1/analysis/clusters` | Clustering |
252252
| `POST /api/v1/analysis/change-points` | Change-point detection |
253-
| `POST /api/v1/analysis/author` | Author attribution |
253+
| `POST /api/v1/analysis/author` | Author attribution — full CLI flag parity (Phase 141): `since`, `detail`, `includeCommits`, `hybrid`, `bm25Weight` all wired through; response is `{ authors, commits? }` |
254254
| `POST /api/v1/analysis/impact` | Impact analysis |
255255
| `POST /api/v1/analysis/semantic-diff` | Semantic diff |
256256
| `POST /api/v1/analysis/semantic-blame` | Semantic blame |

docs/parity.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,11 @@ This table shows less common flags used by specific commands or command groups.
323323
| `--sort-by-date` | `search-history` (MCP only) | bool | false | Sort by first-seen date instead of score |
324324
| `--include-content` | `evolution`, `concept-evolution` | bool | false | Add stored file text in JSON output |
325325
| `--include-commits` | `first-seen` | bool | false | Also search commit messages |
326+
| `--since` | `author` | date || Only consider contributions since this date (HTTP: also accepted by `POST /analysis/author`, Phase 141) |
327+
| `--detail` | `author` | bool | false | Include per-blob contribution detail (HTTP: also accepted by `POST /analysis/author`, Phase 141) |
328+
| `--hybrid` | `author` | bool | false | Use hybrid (vector + BM25) candidate selection (HTTP: also accepted by `POST /analysis/author`, Phase 141) |
329+
| `--bm25-weight` | `author` | float | 0.3 | BM25 weight when `--hybrid` is set (HTTP: also accepted by `POST /analysis/author`, Phase 141) |
330+
| `--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. |
326331

327332
### 2.3 Flag Coherence Issues
328333

@@ -535,7 +540,15 @@ this section's prior open-ended bullets into concrete, numbered
535540
- **Phase 140:** systemic `--model`/`--text-model`/`--code-model` gap
536541
across `analysis.ts` HTTP routes (one shared fix, not 8 per-route ones).
537542
- **Phase 141:** `author` HTTP route full parity (largest single-command
538-
gap found).
543+
gap found). ✅ done — `POST /analysis/author` now accepts `since`,
544+
`detail`, `includeCommits`, `hybrid`, `bm25Weight` (all wired to
545+
`computeAuthorContributions`/`hybridSearch`/`searchCommits`, mirroring the
546+
CLI `author` command exactly) plus `chunks`/`level`/`vss` for flag-surface
547+
parity (accepted, no-op — matching the CLI's own dead-flag behavior for
548+
these three). Response shape changed from a bare array to `{ authors,
549+
commits? }` (breaking change, per §4's parity-over-stability rule) to
550+
carry `includeCommits` results. Model-override triplet intentionally
551+
excluded — tracked separately in Phase 140.
539552
- **Phase 142:** `workflow` HTTP route parity — 3/8 templates exposed
540553
today, all 8 on CLI.
541554
- **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
@@ -15,6 +15,8 @@ import type { Embedding } from '../../core/models/types.js'
1515
import { computeClusters, getBlobHashesOnBranch } from '../../core/search/clustering/clustering.js'
1616
import { computeConceptChangePoints } from '../../core/search/temporal/changePoints.js'
1717
import { computeAuthorContributions } from '../../core/search/authorSearch.js'
18+
import { hybridSearch } from '../../core/search/analysis/hybridSearch.js'
19+
import { searchCommits } from '../../core/search/commitSearch.js'
1820
import { computeExperts } from '../../core/search/experts.js'
1921
import { computeImpact } from '../../core/search/impact.js'
2022
import { parseDateArg } from '../../core/search/temporal/timeSearch.js'
@@ -57,6 +59,19 @@ const AuthorBodySchema = z.object({
5759
topK: z.number().int().positive().optional().default(50),
5860
topAuthors: z.number().int().positive().optional().default(10),
5961
branch: z.string().optional(),
62+
since: z.string().optional(),
63+
detail: z.boolean().optional().default(false),
64+
includeCommits: z.boolean().optional().default(false),
65+
hybrid: z.boolean().optional().default(false),
66+
bm25Weight: z.number().min(0).max(1).optional().default(0.3),
67+
// `chunks`/`level`/`vss` are accepted for CLI flag-surface parity but are
68+
// not wired to anything — `computeAuthorContributions` is blob-level only,
69+
// and the CLI's `author` command itself treats these three the same way
70+
// (declared flags with no effect on results; `--vss` just prints a
71+
// warning). See docs/parity.md for the audit note.
72+
chunks: z.boolean().optional().default(false),
73+
level: z.enum(['file', 'chunk', 'symbol']).optional(),
74+
vss: z.boolean().optional().default(false),
6075
})
6176

6277
const ImpactBodySchema = z.object({
@@ -139,6 +154,15 @@ export function analysisRouter(deps: AnalysisRouterDeps): Router {
139154
return
140155
}
141156
const opts = parsed.data
157+
158+
let since: number | undefined
159+
try {
160+
if (opts.since) since = parseDateArg(opts.since)
161+
} catch (err) {
162+
res.status(400).json({ error: `Date parse error: ${err instanceof Error ? err.message : String(err)}` })
163+
return
164+
}
165+
142166
let queryEmbedding: Embedding
143167
try {
144168
queryEmbedding = await textProvider.embed(opts.query)
@@ -147,12 +171,35 @@ export function analysisRouter(deps: AnalysisRouterDeps): Router {
147171
return
148172
}
149173
try {
174+
// Mirrors the CLI `author` command (src/cli/commands/author.ts): when
175+
// --hybrid is set, use hybridSearch to get pre-scored candidate blobs
176+
// instead of scoring the whole embeddings table by cosine similarity.
177+
let candidateBlobs: Array<{ blobHash: string; score: number }> | undefined
178+
if (opts.hybrid) {
179+
const hybridResults = await hybridSearch(opts.query, queryEmbedding, {
180+
topK: 50,
181+
bm25Weight: opts.bm25Weight,
182+
branch: opts.branch,
183+
})
184+
candidateBlobs = hybridResults.map((r) => ({ blobHash: r.blobHash, score: r.score }))
185+
}
186+
150187
const contributions = await computeAuthorContributions(queryEmbedding, {
151188
topK: opts.topK,
152189
topAuthors: opts.topAuthors,
153190
branch: opts.branch,
191+
since,
192+
detail: opts.detail,
193+
candidateBlobs,
154194
})
155-
res.json(contributions)
195+
196+
if (opts.includeCommits) {
197+
const commitResults = await searchCommits(queryEmbedding, { topK: opts.topK, model: textProvider.model })
198+
res.json({ authors: contributions, commits: commitResults })
199+
return
200+
}
201+
202+
res.json({ authors: contributions })
156203
} catch (err) {
157204
res.status(500).json({ error: err instanceof Error ? err.message : String(err) })
158205
}

tests/serverRoutes.test.ts

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

565-
it('returns 200 with contributions structure (empty DB)', async () => {
565+
it('returns 200 with { authors } structure (empty DB)', async () => {
566566
const res = await request(app)
567567
.post('/api/v1/analysis/author')
568568
.send({ query: 'authentication' })
569569
expect(res.status).toBe(200)
570570
expect(typeof res.body).toBe('object')
571+
expect(Array.isArray(res.body.authors)).toBe(true)
572+
expect(res.body.authors.length).toBe(0)
573+
expect(res.body.commits).toBeUndefined()
574+
})
575+
576+
it('accepts --since (Phase 141) and returns 200 with a valid date', async () => {
577+
const res = await request(app)
578+
.post('/api/v1/analysis/author')
579+
.send({ query: 'authentication', since: '2020-01-01' })
580+
expect(res.status).toBe(200)
581+
expect(Array.isArray(res.body.authors)).toBe(true)
582+
})
583+
584+
it('returns 400 for an unparseable --since value', async () => {
585+
const res = await request(app)
586+
.post('/api/v1/analysis/author')
587+
.send({ query: 'authentication', since: 'not-a-date-at-all-!!' })
588+
expect(res.status).toBe(400)
589+
})
590+
591+
it('accepts --detail (Phase 141) without error', async () => {
592+
const res = await request(app)
593+
.post('/api/v1/analysis/author')
594+
.send({ query: 'authentication', detail: true })
595+
expect(res.status).toBe(200)
596+
expect(Array.isArray(res.body.authors)).toBe(true)
597+
})
598+
599+
it('accepts --hybrid + --bm25-weight (Phase 141) without error', async () => {
600+
const res = await request(app)
601+
.post('/api/v1/analysis/author')
602+
.send({ query: 'authentication', hybrid: true, bm25Weight: 0.5 })
603+
expect(res.status).toBe(200)
604+
expect(Array.isArray(res.body.authors)).toBe(true)
605+
})
606+
607+
it('accepts --include-commits (Phase 141) and returns a commits array', async () => {
608+
const res = await request(app)
609+
.post('/api/v1/analysis/author')
610+
.send({ query: 'authentication', includeCommits: true })
611+
expect(res.status).toBe(200)
612+
expect(Array.isArray(res.body.authors)).toBe(true)
613+
expect(Array.isArray(res.body.commits)).toBe(true)
614+
})
615+
616+
it('accepts chunks/level/vss (Phase 141 flag-surface parity, no-op like the CLI)', async () => {
617+
const res = await request(app)
618+
.post('/api/v1/analysis/author')
619+
.send({ query: 'authentication', chunks: true, level: 'symbol', vss: true })
620+
expect(res.status).toBe(200)
621+
expect(Array.isArray(res.body.authors)).toBe(true)
622+
})
623+
624+
it('returns 400 for an invalid level enum value', async () => {
625+
const res = await request(app)
626+
.post('/api/v1/analysis/author')
627+
.send({ query: 'authentication', level: 'not-a-real-level' })
628+
expect(res.status).toBe(400)
571629
})
572630
})
573631

0 commit comments

Comments
 (0)