@weareikko/code-review 0.8.5 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{cli-DmzA9PxS.js → cli-DK0kteLL.js} +294 -193
- package/dist/cli-DK0kteLL.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/diagnostics.d.ts +11 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/github.d.ts +14 -6
- package/dist/github.d.ts.map +1 -1
- package/dist/gitlab-review.d.ts +9 -0
- package/dist/gitlab-review.d.ts.map +1 -1
- package/dist/otel.d.ts +4 -4
- package/dist/otel.d.ts.map +1 -1
- package/dist/platforms/github.d.ts +7 -4
- package/dist/platforms/github.d.ts.map +1 -1
- package/dist/review.js +1 -1
- package/package.json +1 -1
- package/dist/cli-DmzA9PxS.js.map +0 -1
|
@@ -166,6 +166,7 @@ var REVIEW_THREADS_QUERY = `
|
|
|
166
166
|
pageInfo { hasNextPage endCursor }
|
|
167
167
|
nodes {
|
|
168
168
|
isResolved
|
|
169
|
+
isOutdated
|
|
169
170
|
comments(first: 100) { nodes { databaseId } }
|
|
170
171
|
}
|
|
171
172
|
}
|
|
@@ -338,14 +339,22 @@ var GitHubClient = class {
|
|
|
338
339
|
return parsed.data;
|
|
339
340
|
}
|
|
340
341
|
/**
|
|
341
|
-
* Return the database IDs of review comments that belong to a **
|
|
342
|
-
* review thread. GitHub's REST
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
342
|
+
* Return the database IDs of review comments that belong to a **settled**
|
|
343
|
+
* review thread — one that is either resolved or outdated. GitHub's REST
|
|
344
|
+
* comment endpoints omit both states; they are only exposed via GraphQL
|
|
345
|
+
* `reviewThreads.isResolved` / `isOutdated`. Callers use this set to mark
|
|
346
|
+
* normalized notes resolved so settled threads are excluded from summary
|
|
347
|
+
* carry-over and prior-thread context. Paginates over threads.
|
|
348
|
+
*
|
|
349
|
+
* Outdated counts as settled because GitHub, unlike GitLab, does not
|
|
350
|
+
* auto-resolve a thread when the line it anchors to changes: fixing a finding
|
|
351
|
+
* flips the thread to outdated but leaves `isResolved` false until someone
|
|
352
|
+
* manually resolves it. Treating outdated as settled mirrors GitLab's
|
|
353
|
+
* "automatically resolve outdated diff discussions" behaviour, so a fixed
|
|
354
|
+
* finding stops being re-listed under "Still open from earlier reviews" (#133).
|
|
346
355
|
*/
|
|
347
|
-
async
|
|
348
|
-
const
|
|
356
|
+
async listSettledReviewCommentIds(owner, repo, pull) {
|
|
357
|
+
const settled = /* @__PURE__ */ new Set();
|
|
349
358
|
let cursor = null;
|
|
350
359
|
let hasNext = true;
|
|
351
360
|
while (hasNext) {
|
|
@@ -357,14 +366,14 @@ var GitHubClient = class {
|
|
|
357
366
|
})).repository?.pullRequest?.reviewThreads;
|
|
358
367
|
if (!threads) break;
|
|
359
368
|
for (const thread of threads.nodes ?? []) {
|
|
360
|
-
if (!thread.isResolved) continue;
|
|
361
|
-
for (const comment of thread.comments?.nodes ?? []) if (typeof comment.databaseId === "number")
|
|
369
|
+
if (!thread.isResolved && !thread.isOutdated) continue;
|
|
370
|
+
for (const comment of thread.comments?.nodes ?? []) if (typeof comment.databaseId === "number") settled.add(comment.databaseId);
|
|
362
371
|
}
|
|
363
372
|
hasNext = threads.pageInfo?.hasNextPage ?? false;
|
|
364
373
|
cursor = threads.pageInfo?.endCursor ?? null;
|
|
365
374
|
if (!cursor) hasNext = false;
|
|
366
375
|
}
|
|
367
|
-
return
|
|
376
|
+
return settled;
|
|
368
377
|
}
|
|
369
378
|
};
|
|
370
379
|
//#endregion
|
|
@@ -551,7 +560,7 @@ function buildSummaryBody(summary, costFooter, options = {}) {
|
|
|
551
560
|
return `${withFooter}\n\n${buildSummaryHistoryBlock(historyEntries)}`;
|
|
552
561
|
}
|
|
553
562
|
function buildReviewedCommitFooter(commitSha) {
|
|
554
|
-
return `Reviewed by ${PRODUCT_LINK} v0.
|
|
563
|
+
return `Reviewed by ${PRODUCT_LINK} v0.9.0 for commit ${commitSha}.`;
|
|
555
564
|
}
|
|
556
565
|
function extractReviewedCommitSha(body) {
|
|
557
566
|
return REVIEWED_COMMIT_FOOTER_PATTERN.exec(body)?.[1] ?? null;
|
|
@@ -1350,6 +1359,7 @@ function createDiagnosticContext(phase, config, runId, overrides = {}) {
|
|
|
1350
1359
|
project: config.project,
|
|
1351
1360
|
mr: config.mr,
|
|
1352
1361
|
gitlabUrl: config.gitlabUrl,
|
|
1362
|
+
platform: config.platform,
|
|
1353
1363
|
cwd: config.cwd,
|
|
1354
1364
|
model: config.model,
|
|
1355
1365
|
minSeverity: config.minSeverity,
|
|
@@ -3681,8 +3691,15 @@ async function loadReviewContext(cwd, skillNames = [], warn, options = {}) {
|
|
|
3681
3691
|
]);
|
|
3682
3692
|
const skills = [...discovered];
|
|
3683
3693
|
const discoveredNames = new Set(discovered.map((s) => s.name));
|
|
3684
|
-
const named = await Promise.all(skillNames.filter((n) => !discoveredNames.has(n)).map((n) =>
|
|
3685
|
-
|
|
3694
|
+
const named = await Promise.all(skillNames.filter((n) => !discoveredNames.has(n)).map(async (n) => {
|
|
3695
|
+
try {
|
|
3696
|
+
return await loadNamedSkill(n, cwd, { refresh: options.refreshGitSkills });
|
|
3697
|
+
} catch (error) {
|
|
3698
|
+
warn?.(`Skipping skill "${n}": ${formatError(error)}`);
|
|
3699
|
+
return null;
|
|
3700
|
+
}
|
|
3701
|
+
}));
|
|
3702
|
+
skills.push(...named.filter((s) => s !== null));
|
|
3686
3703
|
return {
|
|
3687
3704
|
conventions,
|
|
3688
3705
|
reviewRules,
|
|
@@ -4338,54 +4355,59 @@ async function runReview(config, options) {
|
|
|
4338
4355
|
attachTelemetry: options.attachTelemetry,
|
|
4339
4356
|
verifyStaged: diskMode ? retrievableSkipped : void 0
|
|
4340
4357
|
};
|
|
4358
|
+
const buildUsage = () => ({
|
|
4359
|
+
model: config.model,
|
|
4360
|
+
thinkingLevel: config.thinkingLevel,
|
|
4361
|
+
tokens: aggregated.tokens,
|
|
4362
|
+
cost: aggregated.cost,
|
|
4363
|
+
byModel: buildByModelUsage(aggregated),
|
|
4364
|
+
skills: context.skills.map((s) => s.name),
|
|
4365
|
+
sizeNotice
|
|
4366
|
+
});
|
|
4341
4367
|
let outputText;
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
const detachTelemetry = options.attachTelemetry?.(findAgent);
|
|
4354
|
-
let turnCount = 0;
|
|
4355
|
-
let toolCallCount = 0;
|
|
4356
|
-
let finalText;
|
|
4357
|
-
try {
|
|
4358
|
-
finalText = await runAgentToCompletion(findAgent, userPrompt, {
|
|
4359
|
-
timeoutMs,
|
|
4360
|
-
onAssistantMessage: (message) => accumulateUsage(aggregated, message, primary.id),
|
|
4361
|
-
onTurnStart: (turn) => {
|
|
4362
|
-
turnCount = turn;
|
|
4363
|
-
logger.debug(`Turn ${turn} started`);
|
|
4364
|
-
},
|
|
4365
|
-
onToolStart: (toolName, args) => {
|
|
4366
|
-
toolCallCount += 1;
|
|
4367
|
-
logger.debug(` → ${toolName}${formatToolArgs(toolName, args)}`);
|
|
4368
|
-
}
|
|
4368
|
+
try {
|
|
4369
|
+
if (config.reviewDepth === "full") {
|
|
4370
|
+
const { findings, summary } = await runMultiAngleFind(context, minSeverity, userPrompt, deps);
|
|
4371
|
+
outputText = await verifyAndSynthesize(findings, summary, diff, options.commitLog, deps);
|
|
4372
|
+
} else {
|
|
4373
|
+
const findAgent = createAgent({
|
|
4374
|
+
systemPrompt,
|
|
4375
|
+
model: primary.model,
|
|
4376
|
+
tools,
|
|
4377
|
+
thinkingLevel: config.thinkingLevel,
|
|
4378
|
+
getApiKey: primary.getApiKey
|
|
4369
4379
|
});
|
|
4370
|
-
|
|
4371
|
-
|
|
4380
|
+
const detachTelemetry = options.attachTelemetry?.(findAgent);
|
|
4381
|
+
let turnCount = 0;
|
|
4382
|
+
let toolCallCount = 0;
|
|
4383
|
+
let finalText;
|
|
4384
|
+
try {
|
|
4385
|
+
finalText = await runAgentToCompletion(findAgent, userPrompt, {
|
|
4386
|
+
timeoutMs,
|
|
4387
|
+
onAssistantMessage: (message) => accumulateUsage(aggregated, message, primary.id),
|
|
4388
|
+
onTurnStart: (turn) => {
|
|
4389
|
+
turnCount = turn;
|
|
4390
|
+
logger.debug(`Turn ${turn} started`);
|
|
4391
|
+
},
|
|
4392
|
+
onToolStart: (toolName, args) => {
|
|
4393
|
+
toolCallCount += 1;
|
|
4394
|
+
logger.debug(` → ${toolName}${formatToolArgs(toolName, args)}`);
|
|
4395
|
+
}
|
|
4396
|
+
});
|
|
4397
|
+
} finally {
|
|
4398
|
+
detachTelemetry?.();
|
|
4399
|
+
}
|
|
4400
|
+
logger.debug(`Agent finished: ${turnCount} turn(s), ${toolCallCount} tool call(s)`);
|
|
4401
|
+
outputText = config.reviewDepth === "verify" ? await runVerifyStage(finalText, diff, options.commitLog, deps) : finalText;
|
|
4372
4402
|
}
|
|
4373
|
-
|
|
4374
|
-
|
|
4403
|
+
} finally {
|
|
4404
|
+
options.onUsage?.(buildUsage());
|
|
4375
4405
|
}
|
|
4376
4406
|
const reviewPath = resolve(cwd, config.reviewFile);
|
|
4377
4407
|
await mkdir(dirname(reviewPath), { recursive: true });
|
|
4378
4408
|
await writeFile(reviewPath, outputText, "utf8");
|
|
4379
4409
|
if (retrievableSkipped.length > 0) await cleanupSkippedDiffs(cwd);
|
|
4380
|
-
return
|
|
4381
|
-
model: config.model,
|
|
4382
|
-
thinkingLevel: config.thinkingLevel,
|
|
4383
|
-
tokens: aggregated.tokens,
|
|
4384
|
-
cost: aggregated.cost,
|
|
4385
|
-
byModel: buildByModelUsage(aggregated),
|
|
4386
|
-
skills: context.skills.map((s) => s.name),
|
|
4387
|
-
sizeNotice
|
|
4388
|
-
};
|
|
4410
|
+
return buildUsage();
|
|
4389
4411
|
}
|
|
4390
4412
|
/**
|
|
4391
4413
|
* Convert the per-model usage buckets into the public {@link ModelUsage} array,
|
|
@@ -4742,13 +4764,13 @@ async function startOtelBridge(options = {}) {
|
|
|
4742
4764
|
unit: "{token}",
|
|
4743
4765
|
advice: { explicitBucketBoundaries: TOKEN_BUCKETS }
|
|
4744
4766
|
});
|
|
4745
|
-
const operationCost = meter.createHistogram("
|
|
4746
|
-
description: "
|
|
4767
|
+
const operationCost = meter.createHistogram("code_review_llm_cost_usd", {
|
|
4768
|
+
description: "LLM cost in USD per turn, by token type (non-standard extension)",
|
|
4747
4769
|
unit: "{usd}",
|
|
4748
4770
|
advice: { explicitBucketBoundaries: COST_BUCKETS_USD }
|
|
4749
4771
|
});
|
|
4750
|
-
const timeToFirstToken = meter.createHistogram("gen_ai.client.
|
|
4751
|
-
description: "Time to first
|
|
4772
|
+
const timeToFirstToken = meter.createHistogram("gen_ai.client.operation.time_to_first_chunk", {
|
|
4773
|
+
description: "Time to first chunk from the LLM",
|
|
4752
4774
|
unit: "s",
|
|
4753
4775
|
advice: { explicitBucketBoundaries: TTFT_BUCKETS_S }
|
|
4754
4776
|
});
|
|
@@ -4788,6 +4810,9 @@ async function startOtelBridge(options = {}) {
|
|
|
4788
4810
|
ciAttrs,
|
|
4789
4811
|
ciSpanAttrs,
|
|
4790
4812
|
model: ctx.model,
|
|
4813
|
+
dryRun: ctx.dryRun,
|
|
4814
|
+
platform: ctx.platform,
|
|
4815
|
+
serverAddress: hostOf(ctx.gitlabUrl),
|
|
4791
4816
|
rootSpanCtx
|
|
4792
4817
|
});
|
|
4793
4818
|
emitReviewStartedLog(logger, ctx, ciAttrs, ciSpanAttrs, rootSpanCtx);
|
|
@@ -4819,50 +4844,55 @@ async function startOtelBridge(options = {}) {
|
|
|
4819
4844
|
entry.span.end();
|
|
4820
4845
|
entry.closed = true;
|
|
4821
4846
|
const status = resolveRunStatus(ctx, isError);
|
|
4822
|
-
const projectPath = ciAttrs["
|
|
4847
|
+
const projectPath = ciAttrs["vcs.repository.name"] ?? "";
|
|
4848
|
+
const vcs = vcsAttrs(ctx.platform, ctx.gitlabUrl);
|
|
4823
4849
|
if (typeof ctx.durationMs === "number") reviewPhaseDuration.record(ctx.durationMs / 1e3, {
|
|
4824
4850
|
...REVIEW_SERVICE_ATTRS,
|
|
4825
|
-
|
|
4826
|
-
"
|
|
4827
|
-
"
|
|
4851
|
+
...vcs,
|
|
4852
|
+
"vcs.repository.name": projectPath,
|
|
4853
|
+
"code_review.phase": ctx.phase,
|
|
4854
|
+
"code_review.status": status
|
|
4828
4855
|
});
|
|
4829
4856
|
if (ctx.phase === ROOT_PHASE) {
|
|
4830
4857
|
const meta = runMeta.get(ctx.runId);
|
|
4831
|
-
const pipelineSource = ciAttrs["
|
|
4858
|
+
const pipelineSource = ciAttrs["cicd.pipeline.source"] ?? "";
|
|
4832
4859
|
const runMetricBase = {
|
|
4833
4860
|
...REVIEW_SERVICE_ATTRS,
|
|
4834
|
-
|
|
4835
|
-
"
|
|
4861
|
+
...vcs,
|
|
4862
|
+
"vcs.repository.name": projectPath,
|
|
4863
|
+
"code_review.dry_run": ctx.dryRun,
|
|
4864
|
+
...ctx.firstReview !== void 0 ? { "code_review.first_review": ctx.firstReview } : {}
|
|
4836
4865
|
};
|
|
4837
4866
|
const usage = meta?.usage ?? ctx.usage;
|
|
4838
4867
|
const runModelAttrs = genAiModelAttrs(void 0, splitModel(usage?.model ?? "").modelId);
|
|
4839
4868
|
reviewRunsTotal.add(1, {
|
|
4840
4869
|
...runMetricBase,
|
|
4841
|
-
"
|
|
4842
|
-
"
|
|
4870
|
+
"cicd.pipeline.source": pipelineSource,
|
|
4871
|
+
"code_review.status": status
|
|
4843
4872
|
});
|
|
4844
4873
|
if (isError) reviewErrorsTotal.add(1, {
|
|
4845
4874
|
...runMetricBase,
|
|
4846
|
-
"
|
|
4875
|
+
"code_review.status": status,
|
|
4847
4876
|
"error.type": errorTypeOf(ctx)
|
|
4848
4877
|
});
|
|
4849
4878
|
if (typeof ctx.durationMs === "number") reviewRunDuration.record(ctx.durationMs / 1e3, {
|
|
4850
4879
|
...runMetricBase,
|
|
4851
4880
|
...runModelAttrs,
|
|
4852
|
-
"
|
|
4853
|
-
"
|
|
4881
|
+
"cicd.pipeline.source": pipelineSource,
|
|
4882
|
+
"code_review.status": status
|
|
4854
4883
|
});
|
|
4855
4884
|
const totalCostUsd = usage?.cost.total;
|
|
4856
4885
|
if (totalCostUsd !== void 0) reviewTotalCost.record(totalCostUsd, {
|
|
4857
4886
|
...runMetricBase,
|
|
4858
4887
|
...runModelAttrs,
|
|
4859
|
-
"
|
|
4888
|
+
"code_review.status": status
|
|
4860
4889
|
});
|
|
4861
4890
|
if (usage) {
|
|
4862
4891
|
const tokenAttrs = {
|
|
4863
4892
|
...REVIEW_SERVICE_ATTRS,
|
|
4864
4893
|
...runModelAttrs,
|
|
4865
|
-
|
|
4894
|
+
...vcs,
|
|
4895
|
+
"vcs.repository.name": projectPath
|
|
4866
4896
|
};
|
|
4867
4897
|
for (const [field, type] of [
|
|
4868
4898
|
["input", "input"],
|
|
@@ -4878,7 +4908,7 @@ async function startOtelBridge(options = {}) {
|
|
|
4878
4908
|
if (bySeverity) {
|
|
4879
4909
|
for (const [severity, count] of Object.entries(bySeverity)) if (count && count > 0) reviewCommentsTotal.add(count, {
|
|
4880
4910
|
...runMetricBase,
|
|
4881
|
-
"
|
|
4911
|
+
"code_review.comment.severity": severity
|
|
4882
4912
|
});
|
|
4883
4913
|
} else {
|
|
4884
4914
|
const posted = ctx.posted ?? 0;
|
|
@@ -4924,16 +4954,17 @@ async function startOtelBridge(options = {}) {
|
|
|
4924
4954
|
context: meta?.rootSpanCtx,
|
|
4925
4955
|
attributes: {
|
|
4926
4956
|
"service.name": SERVICE_NAME,
|
|
4927
|
-
"event.name": "
|
|
4928
|
-
"
|
|
4929
|
-
"
|
|
4930
|
-
"
|
|
4931
|
-
"
|
|
4932
|
-
"
|
|
4957
|
+
"event.name": "code_review.comment",
|
|
4958
|
+
"code_review.run_id": runId,
|
|
4959
|
+
"code_review.comment.file": comment.file,
|
|
4960
|
+
"code_review.comment.line": comment.line,
|
|
4961
|
+
"code_review.comment.severity": comment.severity,
|
|
4962
|
+
"code_review.comment.is_duplicate": duplicate,
|
|
4933
4963
|
...meta && {
|
|
4934
|
-
"
|
|
4935
|
-
"
|
|
4936
|
-
"
|
|
4964
|
+
"vcs.repository.id": meta.project,
|
|
4965
|
+
"vcs.change.id": meta.mr,
|
|
4966
|
+
...meta.platform ? { "vcs.provider.name": meta.platform } : {},
|
|
4967
|
+
...meta.serverAddress ? { "server.address": meta.serverAddress } : {},
|
|
4937
4968
|
...meta.ciAttrs,
|
|
4938
4969
|
...meta.ciSpanAttrs
|
|
4939
4970
|
}
|
|
@@ -4948,20 +4979,30 @@ async function startOtelBridge(options = {}) {
|
|
|
4948
4979
|
ciAttrs,
|
|
4949
4980
|
runId,
|
|
4950
4981
|
configuredModel: runMeta.get(runId)?.model,
|
|
4982
|
+
dryRun: runMeta.get(runId)?.dryRun,
|
|
4983
|
+
platform: runMeta.get(runId)?.platform,
|
|
4984
|
+
serverAddress: runMeta.get(runId)?.serverAddress,
|
|
4951
4985
|
captureContent
|
|
4952
4986
|
});
|
|
4953
4987
|
}
|
|
4954
4988
|
};
|
|
4955
4989
|
}
|
|
4956
4990
|
/**
|
|
4957
|
-
* Builds the dynamic `gen_ai.
|
|
4958
|
-
* shared by the per-turn and per-phase GenAI metric emitters. Owning
|
|
4959
|
-
* one place keeps the two emission sites from drifting into separate
|
|
4960
|
-
* series (the double-count `recordGenAiMetrics` documents).
|
|
4991
|
+
* Builds the dynamic `gen_ai.provider.name` / `gen_ai.request.model` metric
|
|
4992
|
+
* labels shared by the per-turn and per-phase GenAI metric emitters. Owning
|
|
4993
|
+
* these in one place keeps the two emission sites from drifting into separate
|
|
4994
|
+
* Prometheus series (the double-count `recordGenAiMetrics` documents).
|
|
4995
|
+
*
|
|
4996
|
+
* `gen_ai.provider.name` is the current GenAI-semconv discriminator; the
|
|
4997
|
+
* deprecated `gen_ai.system` is emitted alongside it during the transition so
|
|
4998
|
+
* backends still keyed on the old attribute keep working.
|
|
4961
4999
|
*/
|
|
4962
5000
|
function genAiModelAttrs(provider, modelId) {
|
|
4963
5001
|
return {
|
|
4964
|
-
...provider ? {
|
|
5002
|
+
...provider ? {
|
|
5003
|
+
"gen_ai.provider.name": provider,
|
|
5004
|
+
"gen_ai.system": provider
|
|
5005
|
+
} : {},
|
|
4965
5006
|
...modelId ? { "gen_ai.request.model": modelId } : {}
|
|
4966
5007
|
};
|
|
4967
5008
|
}
|
|
@@ -5017,9 +5058,9 @@ function recordTurnUsage(span, u, metricAttrs, tokenUsage, operationCost) {
|
|
|
5017
5058
|
...metricAttrs,
|
|
5018
5059
|
"gen_ai.token.type": "cache_creation"
|
|
5019
5060
|
});
|
|
5020
|
-
span.setAttribute("
|
|
5021
|
-
span.setAttribute("
|
|
5022
|
-
span.setAttribute("
|
|
5061
|
+
span.setAttribute("code_review.cost.input_usd", u.cost.input);
|
|
5062
|
+
span.setAttribute("code_review.cost.output_usd", u.cost.output);
|
|
5063
|
+
span.setAttribute("code_review.cost.total_usd", u.cost.total);
|
|
5023
5064
|
}
|
|
5024
5065
|
}
|
|
5025
5066
|
/**
|
|
@@ -5083,12 +5124,15 @@ function extractOutputMessages(msg) {
|
|
|
5083
5124
|
}]);
|
|
5084
5125
|
}
|
|
5085
5126
|
function buildAgentSubscriber(tracer, tokenUsage, operationCost, timeToFirstToken, reviewerSpanCtx, options = {}) {
|
|
5086
|
-
const { ciAttrs = {}, runId, configuredModel, captureContent = false } = options;
|
|
5127
|
+
const { ciAttrs = {}, runId, configuredModel, dryRun, platform, serverAddress, captureContent = false } = options;
|
|
5087
5128
|
const configuredProvider = configuredModel ? splitModel(configuredModel).provider : void 0;
|
|
5088
5129
|
const baseMetricAttrs = {
|
|
5089
5130
|
"gen_ai.operation.name": "invoke_agent",
|
|
5090
5131
|
...REVIEW_SERVICE_ATTRS,
|
|
5091
|
-
...ciAttrs
|
|
5132
|
+
...ciAttrs,
|
|
5133
|
+
...dryRun !== void 0 ? { "code_review.dry_run": dryRun } : {},
|
|
5134
|
+
...platform ? { "vcs.provider.name": platform } : {},
|
|
5135
|
+
...serverAddress ? { "server.address": serverAddress } : {}
|
|
5092
5136
|
};
|
|
5093
5137
|
return (agent) => {
|
|
5094
5138
|
let currentTurn;
|
|
@@ -5122,13 +5166,16 @@ function buildAgentSubscriber(tracer, tokenUsage, operationCost, timeToFirstToke
|
|
|
5122
5166
|
...baseMetricAttrs,
|
|
5123
5167
|
...genAiModelAttrs(provider, modelId)
|
|
5124
5168
|
};
|
|
5125
|
-
if (provider)
|
|
5169
|
+
if (provider) {
|
|
5170
|
+
span.setAttribute("gen_ai.provider.name", provider);
|
|
5171
|
+
span.setAttribute("gen_ai.system", provider);
|
|
5172
|
+
}
|
|
5126
5173
|
if (modelId) span.setAttribute("gen_ai.response.model", modelId);
|
|
5127
5174
|
if (msg.stopReason) span.setAttribute("gen_ai.response.stop_reason", msg.stopReason);
|
|
5128
5175
|
if (firstTokenMs !== void 0) {
|
|
5129
5176
|
const ttftS = (firstTokenMs - startMs) / 1e3;
|
|
5130
5177
|
timeToFirstToken.record(ttftS, metricAttrs);
|
|
5131
|
-
span.setAttribute("gen_ai.client.
|
|
5178
|
+
span.setAttribute("gen_ai.client.operation.time_to_first_chunk_s", ttftS);
|
|
5132
5179
|
}
|
|
5133
5180
|
if (msg.usage) recordTurnUsage(span, msg.usage, metricAttrs, tokenUsage, operationCost);
|
|
5134
5181
|
if (captureContent) {
|
|
@@ -5199,7 +5246,7 @@ async function loadDefaultRuntime() {
|
|
|
5199
5246
|
const [sdkNode, resources, semconv] = modules;
|
|
5200
5247
|
const serviceResource = resources.resourceFromAttributes({
|
|
5201
5248
|
[semconv.ATTR_SERVICE_NAME ?? "service.name"]: SERVICE_NAME,
|
|
5202
|
-
[semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.
|
|
5249
|
+
[semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.9.0"
|
|
5203
5250
|
});
|
|
5204
5251
|
process.env.OTEL_METRICS_EXPORTER = process.env.OTEL_METRICS_EXPORTER ?? "otlp";
|
|
5205
5252
|
process.env.OTEL_LOGS_EXPORTER = process.env.OTEL_LOGS_EXPORTER ?? "otlp";
|
|
@@ -5221,14 +5268,14 @@ function emitReviewStartedLog(logger, ctx, ciAttrs, ciSpanAttrs, rootSpanCtx) {
|
|
|
5221
5268
|
context: rootSpanCtx,
|
|
5222
5269
|
attributes: {
|
|
5223
5270
|
"service.name": SERVICE_NAME,
|
|
5224
|
-
"event.name": "
|
|
5225
|
-
"
|
|
5226
|
-
"
|
|
5227
|
-
|
|
5271
|
+
"event.name": "code_review.started",
|
|
5272
|
+
"vcs.repository.id": ctx.project,
|
|
5273
|
+
"vcs.change.id": ctx.mr,
|
|
5274
|
+
...vcsAttrs(ctx.platform, ctx.gitlabUrl),
|
|
5228
5275
|
...ciAttrs,
|
|
5229
5276
|
...ciSpanAttrs,
|
|
5230
|
-
"
|
|
5231
|
-
"
|
|
5277
|
+
"code_review.run_id": ctx.runId,
|
|
5278
|
+
"code_review.dry_run": ctx.dryRun,
|
|
5232
5279
|
...modelId !== void 0 && { "gen_ai.request.model": modelId }
|
|
5233
5280
|
}
|
|
5234
5281
|
});
|
|
@@ -5248,10 +5295,10 @@ function emitReviewCompletedLog(logger, ctx, meta, isError) {
|
|
|
5248
5295
|
context: meta?.rootSpanCtx,
|
|
5249
5296
|
attributes: {
|
|
5250
5297
|
"service.name": SERVICE_NAME,
|
|
5251
|
-
"event.name": isError ? "
|
|
5252
|
-
"
|
|
5253
|
-
"
|
|
5254
|
-
|
|
5298
|
+
"event.name": isError ? "code_review.failed" : "code_review.completed",
|
|
5299
|
+
"vcs.repository.id": ctx.project,
|
|
5300
|
+
"vcs.change.id": ctx.mr,
|
|
5301
|
+
...vcsAttrs(ctx.platform, ctx.gitlabUrl),
|
|
5255
5302
|
...meta?.ciAttrs,
|
|
5256
5303
|
...meta?.ciSpanAttrs,
|
|
5257
5304
|
...isError && {
|
|
@@ -5259,15 +5306,15 @@ function emitReviewCompletedLog(logger, ctx, meta, isError) {
|
|
|
5259
5306
|
...ctx.errorInfo && { "error.message": ctx.errorInfo.message },
|
|
5260
5307
|
...typeof ctx.errorInfo?.status === "number" && { "http.response.status_code": ctx.errorInfo.status }
|
|
5261
5308
|
},
|
|
5262
|
-
"
|
|
5263
|
-
"
|
|
5264
|
-
"
|
|
5265
|
-
"
|
|
5266
|
-
"
|
|
5267
|
-
"
|
|
5268
|
-
"
|
|
5309
|
+
"code_review.run_id": ctx.runId,
|
|
5310
|
+
"code_review.duration_ms": ctx.durationMs ?? 0,
|
|
5311
|
+
"code_review.dry_run": ctx.dryRun,
|
|
5312
|
+
"code_review.comments.generated": ctx.generated ?? 0,
|
|
5313
|
+
"code_review.comments.new": ctx.newComments ?? 0,
|
|
5314
|
+
"code_review.comments.duplicate": ctx.duplicateComments ?? 0,
|
|
5315
|
+
"code_review.comments.posted": ctx.posted ?? 0,
|
|
5269
5316
|
...modelId !== void 0 && { "gen_ai.request.model": modelId },
|
|
5270
|
-
...cost !== void 0 && { "
|
|
5317
|
+
...cost !== void 0 && { "code_review.cost.total_usd": cost },
|
|
5271
5318
|
...usage?.tokens.input !== void 0 && { "gen_ai.usage.input_tokens": usage.tokens.input + (usage.tokens.cacheRead ?? 0) },
|
|
5272
5319
|
...usage?.tokens.cacheRead && {
|
|
5273
5320
|
"gen_ai.usage.input_tokens.cached": usage.tokens.cacheRead,
|
|
@@ -5286,39 +5333,39 @@ function spanNameFor(phase) {
|
|
|
5286
5333
|
/** Creates the review-level OTel metric instruments on the given meter. */
|
|
5287
5334
|
function createReviewInstruments(meter) {
|
|
5288
5335
|
return {
|
|
5289
|
-
reviewRunsTotal: meter.createCounter("
|
|
5290
|
-
reviewErrorsTotal: meter.createCounter("
|
|
5336
|
+
reviewRunsTotal: meter.createCounter("code_review_runs_total", { description: "Total number of code-review runs, labelled by terminal status" }),
|
|
5337
|
+
reviewErrorsTotal: meter.createCounter("code_review_errors_total", { description: "Total number of failed code-review runs, labelled by error type" }),
|
|
5291
5338
|
reviewLlmTokens: {
|
|
5292
|
-
input: meter.createCounter("
|
|
5339
|
+
input: meter.createCounter("code_review_llm_input_tokens_total", {
|
|
5293
5340
|
description: "Total non-cached LLM input tokens consumed across code-review runs",
|
|
5294
5341
|
unit: "{token}"
|
|
5295
5342
|
}),
|
|
5296
|
-
output: meter.createCounter("
|
|
5343
|
+
output: meter.createCounter("code_review_llm_output_tokens_total", {
|
|
5297
5344
|
description: "Total LLM output tokens generated across code-review runs",
|
|
5298
5345
|
unit: "{token}"
|
|
5299
5346
|
}),
|
|
5300
|
-
cache_read: meter.createCounter("
|
|
5347
|
+
cache_read: meter.createCounter("code_review_llm_cache_read_tokens_total", {
|
|
5301
5348
|
description: "Total LLM cache-read input tokens across code-review runs",
|
|
5302
5349
|
unit: "{token}"
|
|
5303
5350
|
}),
|
|
5304
|
-
cache_creation: meter.createCounter("
|
|
5351
|
+
cache_creation: meter.createCounter("code_review_llm_cache_creation_tokens_total", {
|
|
5305
5352
|
description: "Total LLM cache-creation input tokens across code-review runs",
|
|
5306
5353
|
unit: "{token}"
|
|
5307
5354
|
})
|
|
5308
5355
|
},
|
|
5309
|
-
reviewRunDuration: meter.createHistogram("
|
|
5356
|
+
reviewRunDuration: meter.createHistogram("code_review_run_duration_seconds", {
|
|
5310
5357
|
description: "Duration of a complete code-review run",
|
|
5311
5358
|
unit: "s",
|
|
5312
5359
|
advice: { explicitBucketBoundaries: REVIEW_RUN_DURATION_BUCKETS_S }
|
|
5313
5360
|
}),
|
|
5314
|
-
reviewTotalCost: meter.createHistogram("
|
|
5361
|
+
reviewTotalCost: meter.createHistogram("code_review_total_cost_usd", {
|
|
5315
5362
|
description: "Total LLM cost in USD for a complete code-review run",
|
|
5316
5363
|
unit: "{usd}",
|
|
5317
5364
|
advice: { explicitBucketBoundaries: REVIEW_TOTAL_COST_BUCKETS_USD }
|
|
5318
5365
|
}),
|
|
5319
|
-
reviewCommentsTotal: meter.createCounter("
|
|
5320
|
-
reviewDraftsPublishedTotal: meter.createCounter("
|
|
5321
|
-
reviewPhaseDuration: meter.createHistogram("
|
|
5366
|
+
reviewCommentsTotal: meter.createCounter("code_review_comments_total", { description: "Total number of MR comments posted by code-review" }),
|
|
5367
|
+
reviewDraftsPublishedTotal: meter.createCounter("code_review_drafts_published_total", { description: "Total number of draft notes published by code-review" }),
|
|
5368
|
+
reviewPhaseDuration: meter.createHistogram("code_review_phase_duration_seconds", {
|
|
5322
5369
|
description: "Duration of individual code-review workflow phases",
|
|
5323
5370
|
unit: "s",
|
|
5324
5371
|
advice: { explicitBucketBoundaries: REVIEW_PHASE_DURATION_BUCKETS_S }
|
|
@@ -5342,7 +5389,7 @@ function errorTypeOf(ctx) {
|
|
|
5342
5389
|
return base;
|
|
5343
5390
|
}
|
|
5344
5391
|
/**
|
|
5345
|
-
* Derives the `
|
|
5392
|
+
* Derives the `code_review.status` label used by review-level OTel metrics.
|
|
5346
5393
|
* Distinguishes timeouts (AbortError / ETIMEDOUT) from generic errors so
|
|
5347
5394
|
* Grafana alerts can treat deadline-exceeded runs separately.
|
|
5348
5395
|
*/
|
|
@@ -5353,55 +5400,91 @@ function resolveRunStatus(ctx, isError) {
|
|
|
5353
5400
|
return "error";
|
|
5354
5401
|
}
|
|
5355
5402
|
/**
|
|
5356
|
-
* Extracts
|
|
5357
|
-
*
|
|
5358
|
-
*
|
|
5403
|
+
* Extracts low-cardinality CI project/pipeline context (repository, owner, base
|
|
5404
|
+
* branch, pipeline trigger) as platform-neutral `vcs.*` / `cicd.*` attributes
|
|
5405
|
+
* added to every metric, span, and log record. Sourced from GitLab CI or GitHub
|
|
5406
|
+
* Actions variables — a run is one or the other, so `??` picks whichever is set;
|
|
5407
|
+
* callers spread the result so missing vars add nothing.
|
|
5359
5408
|
*/
|
|
5360
5409
|
function buildCiAttrs(env) {
|
|
5361
5410
|
const attrs = {};
|
|
5362
|
-
|
|
5363
|
-
if (
|
|
5364
|
-
|
|
5365
|
-
if (
|
|
5411
|
+
const repository = env.CI_PROJECT_PATH ?? env.GITHUB_REPOSITORY;
|
|
5412
|
+
if (repository) attrs["vcs.repository.name"] = repository;
|
|
5413
|
+
const owner = env.CI_PROJECT_NAMESPACE ?? env.GITHUB_REPOSITORY_OWNER;
|
|
5414
|
+
if (owner) attrs["vcs.owner.name"] = owner;
|
|
5415
|
+
const baseRef = env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME ?? env.GITHUB_BASE_REF;
|
|
5416
|
+
if (baseRef) attrs["vcs.ref.base.name"] = baseRef;
|
|
5417
|
+
const pipelineSource = env.CI_PIPELINE_SOURCE ?? env.GITHUB_EVENT_NAME;
|
|
5418
|
+
if (pipelineSource) attrs["cicd.pipeline.source"] = pipelineSource;
|
|
5366
5419
|
return attrs;
|
|
5367
5420
|
}
|
|
5368
5421
|
/**
|
|
5369
|
-
* Extracts high-cardinality
|
|
5370
|
-
*
|
|
5371
|
-
* Prometheus/Mimir).
|
|
5422
|
+
* Extracts high-cardinality CI identifiers that should appear on spans and log
|
|
5423
|
+
* records but NOT on metric data points (to avoid label explosion in
|
|
5424
|
+
* Prometheus/Mimir). Sourced from GitLab CI or GitHub Actions. GitHub exposes no
|
|
5425
|
+
* per-job run id in the environment, so the job's config-key (`GITHUB_JOB`) is
|
|
5426
|
+
* used for the task identifier. Spread via `ciSpanAttrs` stored in RunMeta.
|
|
5372
5427
|
*/
|
|
5373
5428
|
function buildCiSpanAttrs(env) {
|
|
5374
5429
|
const attrs = {};
|
|
5375
|
-
|
|
5376
|
-
if (
|
|
5430
|
+
const taskRunId = env.CI_JOB_ID ?? env.GITHUB_JOB;
|
|
5431
|
+
if (taskRunId) attrs["cicd.pipeline.task.run.id"] = taskRunId;
|
|
5432
|
+
const pipelineRunId = env.CI_PIPELINE_ID ?? env.GITHUB_RUN_ID;
|
|
5433
|
+
if (pipelineRunId) attrs["cicd.pipeline.run.id"] = pipelineRunId;
|
|
5434
|
+
const repositoryUrl = env.CI_PROJECT_URL ?? (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}` : void 0);
|
|
5435
|
+
if (repositoryUrl) attrs["vcs.repository.url.full"] = repositoryUrl;
|
|
5377
5436
|
return attrs;
|
|
5378
5437
|
}
|
|
5438
|
+
/** Host of a server URL, for the low-cardinality `server.address` instance label. */
|
|
5439
|
+
function hostOf(url) {
|
|
5440
|
+
if (!url) return void 0;
|
|
5441
|
+
try {
|
|
5442
|
+
return new URL(url).host;
|
|
5443
|
+
} catch {
|
|
5444
|
+
return;
|
|
5445
|
+
}
|
|
5446
|
+
}
|
|
5447
|
+
/**
|
|
5448
|
+
* Low-cardinality VCS discriminators added to every metric, span, and log:
|
|
5449
|
+
* `vcs.provider.name` (gitlab | github) to filter by platform, and
|
|
5450
|
+
* `server.address` (the instance host) to distinguish multiple GitLab/GitHub
|
|
5451
|
+
* instances (e.g. gitlab.com vs a self-hosted gitlab.example.com).
|
|
5452
|
+
*/
|
|
5453
|
+
function vcsAttrs(platform, serverUrl) {
|
|
5454
|
+
const host = hostOf(serverUrl);
|
|
5455
|
+
return {
|
|
5456
|
+
...platform ? { "vcs.provider.name": platform } : {},
|
|
5457
|
+
...host ? { "server.address": host } : {}
|
|
5458
|
+
};
|
|
5459
|
+
}
|
|
5379
5460
|
function baseAttributes(ctx) {
|
|
5461
|
+
const host = hostOf(ctx.gitlabUrl);
|
|
5380
5462
|
return {
|
|
5381
|
-
"
|
|
5463
|
+
"code_review.run_id": ctx.runId,
|
|
5382
5464
|
"gen_ai.conversation.id": ctx.runId,
|
|
5383
|
-
"
|
|
5384
|
-
"
|
|
5385
|
-
"
|
|
5386
|
-
"
|
|
5387
|
-
"
|
|
5388
|
-
"
|
|
5389
|
-
"
|
|
5465
|
+
"code_review.phase": ctx.phase,
|
|
5466
|
+
"vcs.repository.id": ctx.project,
|
|
5467
|
+
"vcs.change.id": ctx.mr,
|
|
5468
|
+
...ctx.platform ? { "vcs.provider.name": ctx.platform } : {},
|
|
5469
|
+
...host ? { "server.address": host } : {},
|
|
5470
|
+
"code_review.dry_run": ctx.dryRun,
|
|
5471
|
+
"code_review.no_post": ctx.noPost,
|
|
5472
|
+
"code_review.min_severity": ctx.minSeverity
|
|
5390
5473
|
};
|
|
5391
5474
|
}
|
|
5392
5475
|
var NUMERIC_RESULT_ATTRIBUTES = [
|
|
5393
|
-
["durationMs", "
|
|
5394
|
-
["generated", "
|
|
5395
|
-
["newComments", "
|
|
5396
|
-
["duplicateComments", "
|
|
5397
|
-
["posted", "
|
|
5398
|
-
["draftsPublished", "
|
|
5399
|
-
["draftsCreated", "
|
|
5400
|
-
["summaryNoteId", "
|
|
5401
|
-
["warnings", "
|
|
5402
|
-
["draftsAbandoned", "
|
|
5403
|
-
["draftsDeletedPrePublish", "
|
|
5404
|
-
["draftsPublishFailed", "
|
|
5476
|
+
["durationMs", "code_review.duration_ms"],
|
|
5477
|
+
["generated", "code_review.comments.generated"],
|
|
5478
|
+
["newComments", "code_review.comments.new"],
|
|
5479
|
+
["duplicateComments", "code_review.comments.duplicate"],
|
|
5480
|
+
["posted", "code_review.comments.posted"],
|
|
5481
|
+
["draftsPublished", "code_review.drafts.published"],
|
|
5482
|
+
["draftsCreated", "code_review.drafts.created"],
|
|
5483
|
+
["summaryNoteId", "code_review.summary.note_id"],
|
|
5484
|
+
["warnings", "code_review.warnings"],
|
|
5485
|
+
["draftsAbandoned", "code_review.drafts.abandoned"],
|
|
5486
|
+
["draftsDeletedPrePublish", "code_review.drafts.deleted_pre_publish"],
|
|
5487
|
+
["draftsPublishFailed", "code_review.drafts.publish_failed"],
|
|
5405
5488
|
["diffFilesChanged", "diff.files_changed"],
|
|
5406
5489
|
["diffLinesAdded", "diff.lines_added"],
|
|
5407
5490
|
["diffLinesRemoved", "diff.lines_removed"],
|
|
@@ -5409,7 +5492,7 @@ var NUMERIC_RESULT_ATTRIBUTES = [
|
|
|
5409
5492
|
["httpResponseBodySize", "http.response.body.size"]
|
|
5410
5493
|
];
|
|
5411
5494
|
var STRING_RESULT_ATTRIBUTES = [
|
|
5412
|
-
["summaryAction", "
|
|
5495
|
+
["summaryAction", "code_review.summary.action"],
|
|
5413
5496
|
["httpRequestMethod", "http.request.method"],
|
|
5414
5497
|
["httpUrl", "url.full"],
|
|
5415
5498
|
["serverAddress", "server.address"]
|
|
@@ -5426,7 +5509,10 @@ function applyResultAttributes(span, ctx) {
|
|
|
5426
5509
|
}
|
|
5427
5510
|
function applyGenAiAttributes(span, ctx) {
|
|
5428
5511
|
const { provider, modelId } = splitModel(ctx.model ?? "");
|
|
5429
|
-
if (provider)
|
|
5512
|
+
if (provider) {
|
|
5513
|
+
span.setAttribute("gen_ai.provider.name", provider);
|
|
5514
|
+
span.setAttribute("gen_ai.system", provider);
|
|
5515
|
+
}
|
|
5430
5516
|
if (modelId) {
|
|
5431
5517
|
span.setAttribute("gen_ai.request.model", modelId);
|
|
5432
5518
|
span.setAttribute("gen_ai.response.model", modelId);
|
|
@@ -5436,16 +5522,16 @@ function applyGenAiAttributes(span, ctx) {
|
|
|
5436
5522
|
const usage = ctx.usage;
|
|
5437
5523
|
if (!usage) return;
|
|
5438
5524
|
setTokenUsageSpanAttributes(span, usage.tokens);
|
|
5439
|
-
span.setAttribute("
|
|
5440
|
-
span.setAttribute("
|
|
5441
|
-
span.setAttribute("
|
|
5442
|
-
span.setAttribute("
|
|
5443
|
-
span.setAttribute("
|
|
5525
|
+
span.setAttribute("code_review.cost.input_usd", usage.cost.input);
|
|
5526
|
+
span.setAttribute("code_review.cost.output_usd", usage.cost.output);
|
|
5527
|
+
span.setAttribute("code_review.cost.cache_read_usd", usage.cost.cacheRead);
|
|
5528
|
+
span.setAttribute("code_review.cost.cache_creation_usd", usage.cost.cacheWrite);
|
|
5529
|
+
span.setAttribute("code_review.cost.total_usd", usage.cost.total);
|
|
5444
5530
|
}
|
|
5445
5531
|
/**
|
|
5446
5532
|
* Records `gen_ai.client.operation.duration` for the `reviewer.run` phase.
|
|
5447
5533
|
*
|
|
5448
|
-
* Token usage (`gen_ai.client.token.usage`) and cost (`
|
|
5534
|
+
* Token usage (`gen_ai.client.token.usage`) and cost (`code_review_llm_cost_usd`) are
|
|
5449
5535
|
* intentionally NOT recorded here. They are emitted per-turn by
|
|
5450
5536
|
* `buildAgentSubscriber` from the live agent event stream. Keeping a single
|
|
5451
5537
|
* emission point for each metric prevents the double-count that previously
|
|
@@ -5458,7 +5544,9 @@ function recordGenAiMetrics(durationHist, ctx, isError, ciAttrs = {}) {
|
|
|
5458
5544
|
"gen_ai.operation.name": "invoke_agent",
|
|
5459
5545
|
...REVIEW_SERVICE_ATTRS,
|
|
5460
5546
|
...ciAttrs,
|
|
5461
|
-
...
|
|
5547
|
+
...vcsAttrs(ctx.platform, ctx.gitlabUrl),
|
|
5548
|
+
...genAiModelAttrs(provider, modelId),
|
|
5549
|
+
"code_review.dry_run": ctx.dryRun
|
|
5462
5550
|
};
|
|
5463
5551
|
if (isError) attrs["error.type"] = errorTypeOf(ctx);
|
|
5464
5552
|
if (typeof ctx.durationMs === "number") durationHist.record(ctx.durationMs / 1e3, attrs);
|
|
@@ -5631,7 +5719,7 @@ function boldCommentTitle(body) {
|
|
|
5631
5719
|
*/
|
|
5632
5720
|
function buildCommentBody(body, commitSha, confidence) {
|
|
5633
5721
|
const confidenceLine = `_Confidence: ${confidence}._`;
|
|
5634
|
-
const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.
|
|
5722
|
+
const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.9.0 for commit ${commitSha}.</sub>`;
|
|
5635
5723
|
return `${boldCommentTitle(body.trim())}\n\n${confidenceLine}\n\n---\n\n${footer}`;
|
|
5636
5724
|
}
|
|
5637
5725
|
function buildPayload(comment, body, refs, resolved) {
|
|
@@ -5795,11 +5883,14 @@ function reviewCommentPosition(comment) {
|
|
|
5795
5883
|
* comments that render identically on GitHub, so `extractExistingFingerprints`,
|
|
5796
5884
|
* `findExistingSummaryNote`, and the reviewed-commit scan all work as-is.
|
|
5797
5885
|
*
|
|
5798
|
-
* `
|
|
5799
|
-
* threads (from the GraphQL `reviewThreads` query, since
|
|
5800
|
-
*
|
|
5886
|
+
* `settledCommentIds` carries the database ids of comments in settled review
|
|
5887
|
+
* threads — resolved or outdated (from the GraphQL `reviewThreads` query, since
|
|
5888
|
+
* REST omits both). Each such note gets a `resolved` flag mirroring GitLab's
|
|
5889
|
+
* per-note field: an outdated GitHub thread maps to `resolved: true` because
|
|
5890
|
+
* GitHub, unlike GitLab, does not auto-resolve a thread when its anchored line
|
|
5891
|
+
* changes, so treating outdated as resolved matches GitLab's behaviour (#133).
|
|
5801
5892
|
*/
|
|
5802
|
-
function normalizeGitHubDiscussions(reviewComments, issueComments,
|
|
5893
|
+
function normalizeGitHubDiscussions(reviewComments, issueComments, settledCommentIds = /* @__PURE__ */ new Set()) {
|
|
5803
5894
|
const threads = /* @__PURE__ */ new Map();
|
|
5804
5895
|
const order = [];
|
|
5805
5896
|
for (const comment of reviewComments) {
|
|
@@ -5813,7 +5904,7 @@ function normalizeGitHubDiscussions(reviewComments, issueComments, resolvedComme
|
|
|
5813
5904
|
notes.push({
|
|
5814
5905
|
id: comment.id,
|
|
5815
5906
|
body: comment.body ?? "",
|
|
5816
|
-
resolved:
|
|
5907
|
+
resolved: settledCommentIds.has(comment.id),
|
|
5817
5908
|
position: reviewCommentPosition(comment)
|
|
5818
5909
|
});
|
|
5819
5910
|
}
|
|
@@ -5880,12 +5971,12 @@ var GitHubPlatform = class {
|
|
|
5880
5971
|
return refs;
|
|
5881
5972
|
}
|
|
5882
5973
|
async getDiscussions() {
|
|
5883
|
-
const [reviewComments, issueComments,
|
|
5974
|
+
const [reviewComments, issueComments, settledCommentIds] = await Promise.all([
|
|
5884
5975
|
this.client.listReviewComments(this.owner, this.repo, this.pull),
|
|
5885
5976
|
this.client.listIssueComments(this.owner, this.repo, this.pull),
|
|
5886
|
-
this.client.
|
|
5977
|
+
this.client.listSettledReviewCommentIds(this.owner, this.repo, this.pull)
|
|
5887
5978
|
]);
|
|
5888
|
-
return normalizeGitHubDiscussions(reviewComments, issueComments,
|
|
5979
|
+
return normalizeGitHubDiscussions(reviewComments, issueComments, settledCommentIds);
|
|
5889
5980
|
}
|
|
5890
5981
|
buildComments(comments, diff, refs, existingFingerprints) {
|
|
5891
5982
|
this.commitId = refs.head_sha;
|
|
@@ -6423,6 +6514,7 @@ async function run(config, bridges) {
|
|
|
6423
6514
|
const refs = await tracedRead("scm.get_latest_version", () => platform.getRefs());
|
|
6424
6515
|
const initialDiscussions = await tracedRead("scm.get_discussions", () => platform.getDiscussions());
|
|
6425
6516
|
const reviewedCommitSha = findExistingReviewedCommitSha(initialDiscussions);
|
|
6517
|
+
runContext.firstReview = findExistingSummaryNote(initialDiscussions) === null;
|
|
6426
6518
|
if (!config.forceReview && !config.dryRun && !config.noPost && reviewedCommitSha === refs.head_sha) {
|
|
6427
6519
|
const usage = zeroReviewUsage(config.model, config.thinkingLevel);
|
|
6428
6520
|
runContext.usage = usage;
|
|
@@ -6465,27 +6557,36 @@ async function run(config, bridges) {
|
|
|
6465
6557
|
if (priorThreads.length > 0) logger.info(`Found ${priorThreads.length} prior thread(s) with developer replies — including as context.`);
|
|
6466
6558
|
logger.info("Running review...");
|
|
6467
6559
|
let usage;
|
|
6560
|
+
let partialUsage;
|
|
6468
6561
|
try {
|
|
6469
6562
|
usage = await traceDiagnosticPhase("reviewer.run", config, runId, async (context) => {
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6563
|
+
try {
|
|
6564
|
+
const result = await runReview(config, {
|
|
6565
|
+
cwd: config.cwd,
|
|
6566
|
+
diff,
|
|
6567
|
+
commitLog,
|
|
6568
|
+
priorThreads,
|
|
6569
|
+
intent: {
|
|
6570
|
+
title: mr.title,
|
|
6571
|
+
description: mr.description
|
|
6572
|
+
},
|
|
6573
|
+
logger,
|
|
6574
|
+
attachTelemetry: bridges?.otel?.createAgentTelemetry(runId),
|
|
6575
|
+
onUsage: (u) => {
|
|
6576
|
+
partialUsage = u;
|
|
6577
|
+
},
|
|
6578
|
+
sinceRef: config.inputMode === "commits" && reviewedCommitSha && reviewedCommitSha !== refs.head_sha ? reviewedCommitSha : void 0
|
|
6579
|
+
});
|
|
6580
|
+
context.usage = result;
|
|
6581
|
+
return result;
|
|
6582
|
+
} catch (error) {
|
|
6583
|
+
if (partialUsage) context.usage = partialUsage;
|
|
6584
|
+
throw error;
|
|
6585
|
+
}
|
|
6485
6586
|
});
|
|
6486
6587
|
} catch (error) {
|
|
6487
6588
|
if (!isQuotaExceededError(error)) throw error;
|
|
6488
|
-
const skipUsage = zeroReviewUsage(config.model, config.thinkingLevel);
|
|
6589
|
+
const skipUsage = partialUsage ?? zeroReviewUsage(config.model, config.thinkingLevel);
|
|
6489
6590
|
runContext.usage = skipUsage;
|
|
6490
6591
|
runContext.generated = 0;
|
|
6491
6592
|
runContext.newComments = 0;
|
|
@@ -6725,10 +6826,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
6725
6826
|
return;
|
|
6726
6827
|
}
|
|
6727
6828
|
if (argv.includes("--version") || argv.includes("-v")) {
|
|
6728
|
-
console.log("0.
|
|
6829
|
+
console.log("0.9.0");
|
|
6729
6830
|
return;
|
|
6730
6831
|
}
|
|
6731
|
-
process.stderr.write(`[code-review] @weareikko/code-review v0.
|
|
6832
|
+
process.stderr.write(`[code-review] @weareikko/code-review v0.9.0\n`);
|
|
6732
6833
|
assertNodeVersion();
|
|
6733
6834
|
applyCodeReviewEnvPrefix();
|
|
6734
6835
|
applyDefaultCacheRetention();
|
|
@@ -6751,4 +6852,4 @@ if (isDirectRun()) main().catch((error) => {
|
|
|
6751
6852
|
//#endregion
|
|
6752
6853
|
export { normalizeBody as $, SUMMARY_HISTORY_END as A, buildSummaryHistoryEntries as B, createDiagnosticContext as C, traceDiagnosticPhase as D, traceDiagnostic as E, SUMMARY_MARKER as F, findExistingSummaryNoteId as G, extractSummaryHistoryEntries as H, buildArchivedSummaryEntry as I, upsertSummaryNote as J, stripSummaryHistory as K, buildReviewedCommitFooter as L, SUMMARY_HISTORY_ENTRY_START as M, SUMMARY_HISTORY_LIMIT as N, normalizeSeverity as O, SUMMARY_HISTORY_START as P, fingerprints as Q, buildSizeNoticeBlock as R, DIAGNOSTIC_CHANNEL_PREFIX as S, diagnosticChannels as T, findExistingReviewedCommitSha as U, extractReviewedCommitSha as V, findExistingSummaryNote as W, extractDiffHunkContext as X, appendFingerprintMarkers as Y, extractExistingFingerprints as Z, resolveNpmSkillDir as _, main as a, parseReviewMarkdownWithWarnings as b, buildGeneratedComments as c, startOtelBridge as d, sha256 as et, filterDiff as f, parseSkillSpec as g, loadNamedSkill as h, formatUsageLine as i, SUMMARY_HISTORY_ENTRY_END as j, toGitLabReviewSeverity as k, buildPayload as l, gitSkillCacheKey as m, formatPerModelUsage as n, run as o, runReview as p, stripSummaryMarker as q, formatSkillsFooter as r, withHttpStamping as s, countPostedBySeverity as t, isOtelEnabled as u, resolveSkillCacheDir as v, createDiagnosticRunId as w, DIAGNOSTIC_CHANNEL_NAMES as x, parseReviewMarkdown as y, buildSummaryBody as z };
|
|
6753
6854
|
|
|
6754
|
-
//# sourceMappingURL=cli-
|
|
6855
|
+
//# sourceMappingURL=cli-DK0kteLL.js.map
|