diff --git a/dashboard/src/lib/types.ts b/dashboard/src/lib/types.ts index 1fd12b7..5bc1cb0 100644 --- a/dashboard/src/lib/types.ts +++ b/dashboard/src/lib/types.ts @@ -80,6 +80,7 @@ export interface EndpointRecord { costModel?: "per_token" | "per_transaction" | "per_request" | "free"; frequencyClass?: string; batchCapable?: boolean; + inlineParallelCapable?: boolean; cacheCapable?: boolean; streaming?: boolean; isMiddleware?: boolean; diff --git a/dashboard/src/pages/Endpoints.tsx b/dashboard/src/pages/Endpoints.tsx index 62f3d10..528a169 100644 --- a/dashboard/src/pages/Endpoints.tsx +++ b/dashboard/src/pages/Endpoints.tsx @@ -76,6 +76,7 @@ function EndpointCard({ ep }: { ep: EndpointRecord }) { const caps: string[] = []; if (ep.streaming) caps.push('stream'); if (ep.batchCapable) caps.push('batch'); + if (ep.inlineParallelCapable) caps.push('inline-parallel'); if (ep.cacheCapable) caps.push('cache'); if (ep.isMiddleware) caps.push('middleware'); diff --git a/docs/superpowers/plans/PROGRESS.md b/docs/superpowers/plans/PROGRESS.md index a52c614..ec4cf67 100644 --- a/docs/superpowers/plans/PROGRESS.md +++ b/docs/superpowers/plans/PROGRESS.md @@ -130,7 +130,7 @@ Two trackers in one file: Post-foundation work organized into 10 waves + 2 standalones, tracked via `wave/*` + `area/*` GitHub labels. **Execution order is severity-weighted** (platform issues outrank accuracy): Wave 9 → 6 → 7 → 8 → 10 → standalones → accuracy waves (5 → 3 → 1 → 2 → 4). -**As of 2026-05-27:** all platform waves (6–10) are SHIPPED, plus accuracy Wave 3. Remaining open work: accuracy waves **5 → 1 → 2 → 4** + standalones #45/#52. +**As of 2026-05-29:** all platform waves (6–10) are SHIPPED, plus accuracy Waves 3, 5, and 1. Remaining open work: accuracy waves **2 → 4** + Wave 3 follow-ups #127/#128 + standalones #45/#52. ## Overall Wave Status @@ -143,10 +143,10 @@ Post-foundation work organized into 10 waves + 2 standalones, tracked via `wave/ | **10** | Config hygiene | #97, #98 | 🟢 | Shipped via PR [#124](https://github.com/recost-dev/extension/pull/124). | | — | Standalone #45 | #45 | ⬜ | Opt-in Project ID persistence — independent feature. | | — | Standalone #52 | #52 | ⬜ | Dashboard theming — can defer indefinitely. | -| **5** | Housekeeping (accuracy) | #118, #119 | ⬜ | Cheapest accuracy work. Both in-repo (benchmark runner + `benchmark.yml`). | -| **3** | Resolver follow-ups (accuracy) | #114, #115, #116 | 🟢 | Shipped via PR [#126](https://github.com/recost-dev/extension/pull/126). Follow-ups #127 (detection threading) + #128 (dashboard badge). | -| **1** | Findings quality (accuracy) | #84, #85, #112 | 🟡 | Code complete + reviewed; full benchmark Δ +0.00pp. Awaits manual EDH check of the two UI bits (confidence filter, sources badge). | -| **2** | Traceability (accuracy) | #81, #113 | ⬜ | Corpus expansion + dual locations. | +| **5** | Housekeeping (accuracy) | #118, #119 | 🟢 | Both closed 2026-05-27 (benchmark runner sort + D1 CI gate verified). | +| **3** | Resolver follow-ups (accuracy) | #114, #115, #116 | 🟢 | Shipped via PR [#126](https://github.com/recost-dev/extension/pull/126). Follow-ups #127 (detection threading) + #128 (dashboard badge) landed on `feat/wave3-followups`. | +| **1** | Findings quality (accuracy) | #84, #85, #112 | 🟢 | All closed 2026-05-28 (C2 dedupe, C3 confidence, CACHE/BATCH_GUARD tightening). | +| **2** | Traceability (accuracy) | #81, #113 | ⬜ | Corpus expansion + dual locations. Next wave. | | **4** | Recall recovery (accuracy) | #117 | ⬜ | Risky — depended on Wave 3 (#116), now unblocked. | ## Wave 9 — Local-mode IPC architecture (CLOSED) @@ -232,6 +232,8 @@ Post-foundation work organized into 10 waves + 2 standalones, tracked via `wave/ > Append `YYYY-MM-DD HH:MM — `. Newest at top. +- 2026-05-29 — **Wave 3 follow-ups #127 + #128 landed** on `feat/wave3-followups`. #127 (`a1972bf`): threaded `inlineParallelCapable` through the regex pattern path (`openai-compatible.ts` emit, registry-only), the regex-only `local-waste-detector.ts` (new inline-parallel finding with the n/count suggestion, distinct `inline_parallel` id), the pattern dedup key (`utils.ts`), and the intelligence graph (`ApiCallNode` + `builder.ts`). #128 (`e9d7d7e`): `EndpointRecord` in both UI type files + `Endpoints.tsx` inline-parallel chip + `scan-results.ts` population at all 3 endpoint-construction sites — restores the indicator DALL·E lost when #116 reclassified `images.generate` off `batchCapable`. Gates: full `test:scanner` green (3 new tests), `build` (webview+ext) clean, benchmark Δ +0.00pp on all 5 metrics. **Caveat:** the web dashboard reads endpoints from the API, whose schema has no `inline_parallel` column — dashboard chip needs an api-repo migration to light up; VS Code webview badge works now. Next: accuracy Wave 2 (#113 → #81). +- 2026-05-29 — **Tracker reconciliation.** Waves 5 (#118/#119, closed 05-27) and 1 (#84/#85/#112, closed 05-28) marked 🟢 — both had merged but were never reflected here. Remaining accuracy work is now Wave 2 → 4. - 2026-05-28 — **Wave 1 (findings quality) code-complete** on `claude/superpowers-plugins-skills-1Yaij` (plan `docs/superpowers/plans/2026-05-28-wave1-findings-quality.md`). #112: comment-stripped guard window stops the `CACHE_GUARD`/`BATCH_GUARD` literal-word leak (URL-safe `//` lookbehind). #85: detectors carry a structural `riskScore`; single `deriveSeverity()` (hybrid floor+amplifier, not pure confidence×cost) + `computeCostImpact()` applied at all 5 `Suggestion`-construction sites; `costImpactUsd` internal-only; confidence filter in the sidebar. #84: `collapseSuggestions()` dedupes by `type::file::endpoint|line-bucket`, unions `sources`, max confidence, AI-preferred description; `mergeAiSuggestions` collapses instead of dropping; "detected by N sources" badge. Subagent-driven (impl + spec + code-quality review per unit). Gates: full `test:scanner` green, full benchmark Δ +0.00pp on all 5 metrics. **Pending:** (1) manual EDH check of the two UI bits; (2) follow-ups — pre-existing `scope === "internal"` guard divergence between the two `buildAggressiveSuggestions` copies (scan-results.ts lacks it), and dead `SourceBadge` + duplicated badge inline-style in `ResultsPage.tsx` (extract a shared secondary-badge). - 2026-05-27 — **Wave 3 shipped (PR #126)** — #114/#115/#116 closed. Subagent-driven across 2 parallel worktree tracks; gates green (test:scanner, build:ext, benchmark Δ +0.00pp). Follow-ups filed: #127 (detection threading) + #128 (dashboard badge). **Tracker reconciliation:** also corrected stale statuses — Waves 7 (PR #122), 8 (PR #123), and 10 (PR #124) had merged earlier but were never marked 🟢 here. All platform waves (6–10) now confirmed shipped; remaining work is accuracy waves 5→1→2→4 + standalones. - 2026-05-21 — **Wave 7 PR opened (#122).** Closes #92 + #93 as design-resolved via labeling, not reconciliation. 7 commits on `wave7/cost-simulator-labeling` (worktree `../extension-wave7`): shared `EstimateDisclaimer` component, render on Simulate tab, code comments on `LOCAL_PRICING` + `FREQUENCY_CLASS_MULTIPLIERS`, CLAUDE.md "Cost numbers: heuristic vs authoritative" section. All gates green (build, 4/4 tests, D1 Δ +0.00pp). Spec + plan committed to main. Wave 8 (#46/#94/#100) is next per severity order. diff --git a/src/intelligence/__tests__/builder.test.ts b/src/intelligence/__tests__/builder.test.ts index 5f038dd..6ceaf48 100644 --- a/src/intelligence/__tests__/builder.test.ts +++ b/src/intelligence/__tests__/builder.test.ts @@ -25,6 +25,7 @@ run("buildRepoIntelligenceSnapshot normalizes paths, stores null providers, and library: "openai", cacheCapable: false, batchCapable: false, + inlineParallelCapable: true, }, { file: "./src/payments.ts", @@ -127,11 +128,14 @@ run("buildRepoIntelligenceSnapshot normalizes paths, stores null providers, and assert.equal(workerCall.provider, "openai"); assert.equal(workerCall.library, "openai"); assert.equal(workerCall.filePath, "src/worker.ts"); + assert.equal(workerCall.inlineParallelCapable, true); const stripeCall = Object.values(snapshot.apiCalls).find((apiCall) => apiCall.filePath === "src/payments.ts" && apiCall.line === 15); assert.ok(stripeCall); assert.equal(stripeCall.provider, "stripe"); assert.equal(stripeCall.library, "stripe"); + // Absent on input → coerced to false (parallels batchCapable). + assert.equal(stripeCall.inlineParallelCapable, false); const regexOnlyCall = Object.values(snapshot.apiCalls).find((apiCall) => apiCall.filePath === "src/regex-only.ts" && apiCall.line === 22); assert.ok(regexOnlyCall); diff --git a/src/intelligence/builder.ts b/src/intelligence/builder.ts index 8f6aceb..9a50c83 100644 --- a/src/intelligence/builder.ts +++ b/src/intelligence/builder.ts @@ -197,6 +197,7 @@ export function buildRepoIntelligenceSnapshot( costModel: call.costModel ?? null, frequencyClass: call.frequencyClass ?? null, batchCapable: Boolean(call.batchCapable), + inlineParallelCapable: Boolean(call.inlineParallelCapable), cacheCapable: Boolean(call.cacheCapable), streaming: Boolean(call.streaming), isMiddleware: Boolean(call.isMiddleware), diff --git a/src/intelligence/types.ts b/src/intelligence/types.ts index 6d85363..3182dc1 100644 --- a/src/intelligence/types.ts +++ b/src/intelligence/types.ts @@ -22,6 +22,7 @@ export interface ApiCallNode { costModel: ApiCallInput["costModel"] | null; frequencyClass: ApiCallInput["frequencyClass"] | null; batchCapable: boolean; + inlineParallelCapable: boolean; cacheCapable: boolean; streaming: boolean; isMiddleware: boolean; diff --git a/src/scan-results.ts b/src/scan-results.ts index 8cf2478..c3027dd 100644 --- a/src/scan-results.ts +++ b/src/scan-results.ts @@ -492,6 +492,7 @@ export function mergeRemoteAndLocalEndpoints( if (!endpoint.costModel && call.costModel) endpoint.costModel = call.costModel; endpoint.frequencyClass = pickMostSevereFrequency(endpoint.frequencyClass, call.frequencyClass); if (call.batchCapable) endpoint.batchCapable = true; + if (call.inlineParallelCapable) endpoint.inlineParallelCapable = true; if (call.cacheCapable) endpoint.cacheCapable = true; if (call.streaming) endpoint.streaming = true; if (call.isMiddleware) endpoint.isMiddleware = true; @@ -547,6 +548,7 @@ export function mergeRemoteAndLocalEndpoints( costModel: call.costModel, frequencyClass: call.frequencyClass, batchCapable: call.batchCapable, + inlineParallelCapable: call.inlineParallelCapable, cacheCapable: call.cacheCapable, streaming: call.streaming, isMiddleware: call.isMiddleware, @@ -580,6 +582,7 @@ export function mergeRemoteAndLocalEndpoints( if (!synthetic.costModel && call.costModel) synthetic.costModel = call.costModel; synthetic.frequencyClass = pickMostSevereFrequency(synthetic.frequencyClass, call.frequencyClass); if (call.batchCapable) synthetic.batchCapable = true; + if (call.inlineParallelCapable) synthetic.inlineParallelCapable = true; if (call.cacheCapable) synthetic.cacheCapable = true; if (call.streaming) synthetic.streaming = true; if (call.isMiddleware) synthetic.isMiddleware = true; diff --git a/src/scanner/local-waste-detector.ts b/src/scanner/local-waste-detector.ts index de4a024..03b98ac 100644 --- a/src/scanner/local-waste-detector.ts +++ b/src/scanner/local-waste-detector.ts @@ -308,6 +308,43 @@ function detectBatchFinding(relativePath: string, site: MatchedCallSite): LocalW ); } +// Inline-parallel: the endpoint itself accepts an n/count parameter (a +// registry-only signal, e.g. DALL·E `images.generate`), so fanning out one +// request per item is wasteful. Mirrors the AST inline-parallel detector for +// the regex-only path (RECOST_DISABLE_AST=1). Distinct from `detectBatchFinding` +// — uses an `inline_parallel` id so it never collapses with a batch finding. +function detectInlineParallelFinding(relativePath: string, site: MatchedCallSite): LocalWasteFinding | null { + const ext = path.extname(relativePath).toLowerCase(); + if (!JS_TS_EXTENSIONS.has(ext)) return null; + if (!site.match.inlineParallelCapable) return null; + if (site.batchGuard) return null; + if (!(site.loopDepth > 0 || site.promiseAll || site.mapFanout || site.arrayFanout)) return null; + + const evidence: string[] = []; + pushEvidence(evidence, site.loopDepth > 0, "Call executes inside a loop."); + pushEvidence( + evidence, + site.promiseAll || site.mapFanout || site.arrayFanout, + "Parallel collection fanout issues one request per item." + ); + evidence.push("This endpoint accepts an n/count parameter that returns multiple results from a single request."); + const score = baseScore(site) + 1 - (site.smallBounded ? 1 : 0); + const confidence = scoreToConfidence(score, site, true); + if (confidence < 0.35) return null; + return { + id: `local-inline_parallel-${relativePath}:${site.line}`, + type: "batch" as SuggestionType, + severity: scoreToSeverity(score), + confidence, + riskScore: score, + description: + "This endpoint accepts an n/count parameter — request multiple results in a single call instead of issuing one request per item.", + affectedFile: relativePath, + line: site.line, + evidence, + }; +} + function detectRedundancyFinding(relativePath: string, site: MatchedCallSite, clientInitCount: number): LocalWasteFinding | null { const ext = path.extname(relativePath).toLowerCase(); if (!JS_TS_EXTENSIONS.has(ext)) return null; @@ -415,7 +452,11 @@ function dedupeFindings(findings: Array): LocalWasteFi const deduped = new Map(); for (const finding of findings) { if (!finding) continue; - const key = `${finding.type}:${finding.affectedFile}:${finding.line ?? 0}`; + // Key on the detector-specific id so distinct detectors that share a + // SuggestionType at the same site stay separate — e.g. batch + // (`local-batch-…`) vs inline-parallel (`local-inline_parallel-…`), which + // both emit type "batch". Fall back to type:file:line if an id is missing. + const key = finding.id || `${finding.type}:${finding.affectedFile}:${finding.line ?? 0}`; const existing = deduped.get(key); if (!existing || finding.confidence > existing.confidence) { deduped.set(key, finding); @@ -433,6 +474,7 @@ export function detectLocalWasteFindingsInText(relativePath: string, text: strin for (const site of callSites) { findings.push(detectCacheFinding(relativePath, site)); findings.push(detectBatchFinding(relativePath, site)); + findings.push(detectInlineParallelFinding(relativePath, site)); findings.push(detectRedundancyFinding(relativePath, site, clientInitCount)); findings.push(detectNPlusOneFinding(relativePath, site)); findings.push(detectRateLimitFinding(relativePath, site)); diff --git a/src/scanner/patterns/openai-compatible.ts b/src/scanner/patterns/openai-compatible.ts index 8b4a82e..c904564 100644 --- a/src/scanner/patterns/openai-compatible.ts +++ b/src/scanner/patterns/openai-compatible.ts @@ -173,6 +173,9 @@ export const openAiCompatibleMatcher: LineMatcher = { const streaming = reg?.streaming ?? /stream/i.test(action); const batchCapable = reg?.batchCapable ?? /batches|batch/.test(path); + // No hint-regex fallback: inline-parallel capability (an n/count param) + // is a registry-only signal — there is no reliable lexical hint for it. + const inlineParallelCapable = reg?.inlineParallelCapable ?? false; const cacheCapable = reg?.cacheCapable ?? /responses|chat|assistants|threads/.test(path); results.push({ @@ -187,6 +190,7 @@ export const openAiCompatibleMatcher: LineMatcher = { host, streaming, batchCapable, + inlineParallelCapable, cacheCapable, rawMatch: match[0], }); diff --git a/src/scanner/patterns/types.ts b/src/scanner/patterns/types.ts index bacbf9d..5899e52 100644 --- a/src/scanner/patterns/types.ts +++ b/src/scanner/patterns/types.ts @@ -22,6 +22,7 @@ export interface ApiCallMatch { loopContext?: boolean; streaming?: boolean; batchCapable?: boolean; + inlineParallelCapable?: boolean; cacheCapable?: boolean; inferredCostRisk?: string[]; rawMatch?: string; diff --git a/src/scanner/patterns/utils.ts b/src/scanner/patterns/utils.ts index 2787a97..fe1f648 100644 --- a/src/scanner/patterns/utils.ts +++ b/src/scanner/patterns/utils.ts @@ -39,6 +39,7 @@ export function uniqueMatches(matches: ApiCallMatch[]): ApiCallMatch[] { match.host ?? "", match.streaming ? "1" : "0", match.batchCapable ? "1" : "0", + match.inlineParallelCapable ? "1" : "0", ].join("|"); if (seen.has(key)) continue; diff --git a/src/test/local-waste-detector.test.ts b/src/test/local-waste-detector.test.ts index d99f9d9..b1a4156 100644 --- a/src/test/local-waste-detector.test.ts +++ b/src/test/local-waste-detector.test.ts @@ -59,6 +59,18 @@ run("reduces confidence when polling already has backoff and concurrency guards" assert.ok((rateLimit?.confidence ?? 1) < 0.75); }); +run("flags inline-parallel fanout for an n/count-capable endpoint (regex-only path)", () => { + const text = [ + "async function makeThumbnails(prompts) {", + " return Promise.all(prompts.map((prompt) => client.images.generate({ prompt })));", + "}", + ].join("\n"); + const findings = detectLocalWasteFindingsInText("src/lib/thumbnails.ts", text); + const inlineParallel = findings.find((finding) => finding.id.includes("inline_parallel")); + assert.ok(inlineParallel, "expected an inline-parallel finding for images.generate fanout"); + assert.match(inlineParallel?.description ?? "", /n\/count parameter/); +}); + run("#112: bare 'cache' in a comment does not suppress a cache finding", () => { const text = [ "// we should cache this someday but do not yet", diff --git a/src/test/scanner-patterns.test.ts b/src/test/scanner-patterns.test.ts index ed76c3d..87ab956 100644 --- a/src/test/scanner-patterns.test.ts +++ b/src/test/scanner-patterns.test.ts @@ -99,4 +99,18 @@ run("keeps route definition detection", () => { assert.ok(routes.some((r) => r.method === "POST" && r.url === "/webhook/stripe")); }); +run("emits inlineParallelCapable from the registry for images.generate", () => { + const match = pickProvider("await client.images.generate({ prompt: 'a cat', n: 4 });", "openai"); + assert.ok(match); + assert.equal(match?.inlineParallelCapable, true); + // images.generate is inline-parallel, not batch-capable (reclassified in #116). + assert.notEqual(match?.batchCapable, true); +}); + +run("leaves inlineParallelCapable unset for non-inline-parallel methods", () => { + const match = pickProvider("await client.chat.completions.create({ model: 'gpt-4.1' });", "openai"); + assert.ok(match); + assert.notEqual(match?.inlineParallelCapable, true); +}); + console.log("All scanner matcher tests passed"); diff --git a/webview/src/types.ts b/webview/src/types.ts index 614184c..a39ddc6 100644 --- a/webview/src/types.ts +++ b/webview/src/types.ts @@ -43,6 +43,7 @@ export interface EndpointRecord { costModel?: "per_token" | "per_transaction" | "per_request" | "free"; frequencyClass?: string; batchCapable?: boolean; + inlineParallelCapable?: boolean; cacheCapable?: boolean; streaming?: boolean; isMiddleware?: boolean;