opencode-magi 0.10.0 → 0.12.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/README.ja.md +254 -0
- package/README.md +31 -26
- package/dist/commands.js +11 -9
- package/dist/config/index.js +2 -0
- package/dist/config/resolve.js +120 -246
- package/dist/config/validate.js +98 -1110
- package/dist/constant.js +131 -0
- package/dist/graphql/index.generated.js +125 -0
- package/dist/graphql/index.js +49 -0
- package/dist/index.js +12 -800
- package/dist/magi.js +357 -0
- package/dist/permissions/common.json +22 -22
- package/dist/permissions/editor.json +12 -12
- package/dist/prompts/index.js +98 -0
- package/dist/prompts/merge/ci-classification/output-contract.md +22 -0
- package/dist/prompts/merge/ci-classification/task.md +10 -0
- package/dist/prompts/merge/ci-classification/validate.json +20 -0
- package/dist/prompts/merge/conflict/output-contract.md +12 -0
- package/dist/prompts/merge/conflict/task.md +9 -0
- package/dist/prompts/merge/conflict/validate.json +4 -0
- package/dist/prompts/merge/edit/output-contract.md +26 -0
- package/dist/prompts/merge/edit/task.md +10 -0
- package/dist/prompts/merge/edit/validate.json +60 -0
- package/dist/prompts/review/ci-classification/output-contract.md +21 -0
- package/dist/prompts/review/ci-classification/task.md +7 -0
- package/dist/prompts/review/ci-classification/validate.json +20 -0
- package/dist/prompts/review/close-reconsideration/output-contract.md +21 -0
- package/dist/prompts/review/close-reconsideration/task.md +6 -0
- package/dist/prompts/review/close-reconsideration/validate.json +44 -0
- package/dist/prompts/review/comment/output-contract.md +11 -0
- package/dist/prompts/review/comment/task.md +3 -0
- package/dist/prompts/review/comment/validate.json +8 -0
- package/dist/prompts/review/finding-validation/output-contract.md +25 -0
- package/dist/prompts/{templates/review/finding-validation.md → review/finding-validation/task.md} +2 -3
- package/dist/prompts/review/finding-validation/validate.json +21 -0
- package/dist/prompts/review/rereview/output-contract.md +30 -0
- package/dist/prompts/{templates/review/rereview.md → review/rereview/task.md} +8 -11
- package/dist/prompts/review/rereview/validate.json +87 -0
- package/dist/prompts/review/review/output-contract.md +25 -0
- package/dist/prompts/{templates/review/review.md → review/review/task.md} +2 -5
- package/dist/prompts/review/review/validate.json +54 -0
- package/dist/prompts/triage/acceptance/output-contract.md +16 -0
- package/dist/prompts/triage/acceptance/validate.json +23 -0
- package/dist/prompts/triage/category/output-contract.md +16 -0
- package/dist/prompts/triage/category/validate.json +23 -0
- package/dist/prompts/triage/comment-classification/output-contract.md +15 -0
- package/dist/prompts/triage/comment-classification/validate.json +28 -0
- package/dist/prompts/triage/create/output-contract.md +28 -0
- package/dist/prompts/triage/create/validate.json +73 -0
- package/dist/prompts/triage/deplicate/output-contract.md +16 -0
- package/dist/prompts/triage/deplicate/validate.json +20 -0
- package/dist/prompts/triage/existing/output-contract.md +16 -0
- package/dist/prompts/triage/existing/validate.json +13 -0
- package/dist/prompts/triage/reconsider/output-contract.md +16 -0
- package/dist/prompts/triage/reconsider/validate.json +23 -0
- package/dist/prompts/triage/signal/output-contract.md +19 -0
- package/dist/prompts/triage/signal/validate.json +18 -0
- package/dist/tools/clear/index.js +21 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/merge/action.js +47 -0
- package/dist/tools/merge/context.js +116 -0
- package/dist/tools/merge/editor.js +256 -0
- package/dist/tools/merge/index.js +151 -0
- package/dist/tools/merge/index.type.js +1 -0
- package/dist/tools/merge/merge.js +42 -0
- package/dist/tools/merge/report.js +73 -0
- package/dist/tools/review/action.js +459 -0
- package/dist/tools/review/check.js +335 -0
- package/dist/tools/review/context.js +254 -0
- package/dist/tools/review/index.js +104 -0
- package/dist/tools/review/index.type.js +1 -0
- package/dist/tools/review/report.js +143 -0
- package/dist/tools/review/review.js +146 -0
- package/dist/tools/review/reviewer.js +409 -0
- package/dist/tools/triage/index.js +16 -0
- package/dist/tools/validate/index.js +28 -0
- package/dist/utils/array.js +6 -0
- package/dist/utils/assertion.js +38 -0
- package/dist/utils/exec.js +14 -0
- package/dist/utils/fs.js +24 -0
- package/dist/utils/function.js +46 -0
- package/dist/utils/github.js +63 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/index.type.js +1 -0
- package/dist/utils/object.js +21 -0
- package/dist/utils/opencode.js +25 -0
- package/dist/utils/string.js +38 -0
- package/dist/utils/worker.js +30 -0
- package/package.json +36 -23
- package/schema.json +116 -20
- package/dist/config/load.js +0 -62
- package/dist/config/output.js +0 -25
- package/dist/config/worktree.js +0 -25
- package/dist/github/commands.js +0 -905
- package/dist/github/retry.js +0 -44
- package/dist/orchestrator/abort.js +0 -9
- package/dist/orchestrator/ci.js +0 -579
- package/dist/orchestrator/findings.js +0 -88
- package/dist/orchestrator/inline-comments.js +0 -73
- package/dist/orchestrator/majority.js +0 -62
- package/dist/orchestrator/merge.js +0 -1260
- package/dist/orchestrator/model.js +0 -216
- package/dist/orchestrator/pool.js +0 -15
- package/dist/orchestrator/report.js +0 -175
- package/dist/orchestrator/review-context.js +0 -342
- package/dist/orchestrator/review.js +0 -1480
- package/dist/orchestrator/run-manager.js +0 -2132
- package/dist/orchestrator/safety.js +0 -44
- package/dist/orchestrator/triage.js +0 -1370
- package/dist/prompts/compose.js +0 -473
- package/dist/prompts/contracts.js +0 -300
- package/dist/prompts/output.js +0 -401
- package/dist/prompts/templates/merge/ci-classification.md +0 -16
- package/dist/prompts/templates/merge/conflict.md +0 -10
- package/dist/prompts/templates/merge/edit.md +0 -15
- package/dist/prompts/templates/review/ci-classification.md +0 -9
- package/dist/prompts/templates/review/close-reconsideration.md +0 -6
- /package/dist/{types.js → config/index.type.js} +0 -0
- /package/dist/prompts/{templates/triage/acceptance.md → triage/acceptance/task.md} +0 -0
- /package/dist/prompts/{templates/triage/category.md → triage/category/task.md} +0 -0
- /package/dist/prompts/{templates/triage/comment-classification.md → triage/comment-classification/task.md} +0 -0
- /package/dist/prompts/{templates/triage/create.md → triage/create/task.md} +0 -0
- /package/dist/prompts/{templates/triage/duplicate.md → triage/deplicate/task.md} +0 -0
- /package/dist/prompts/{templates/triage/existing-pr.md → triage/existing/task.md} +0 -0
- /package/dist/prompts/{templates/triage/reconsider.md → triage/reconsider/task.md} +0 -0
- /package/dist/prompts/{templates/triage/signal.md → triage/signal/task.md} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { MagiError } from "@/magi";
|
|
4
|
+
import { filterEmpty, toTitleCase } from "@/utils";
|
|
5
|
+
export async function createReport(e) {
|
|
6
|
+
this.context.abort.throwIfAborted();
|
|
7
|
+
if (!e) {
|
|
8
|
+
const status = "completed";
|
|
9
|
+
const text = await createContent.call(this, { status });
|
|
10
|
+
await writeFile(join(this.state.output, "report.md"), `${text}\n`);
|
|
11
|
+
await this.updateState({
|
|
12
|
+
completedAt: new Date().toISOString(),
|
|
13
|
+
status,
|
|
14
|
+
});
|
|
15
|
+
return text;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const error = e instanceof Error ? e.message : "Unknown error";
|
|
19
|
+
const status = e instanceof MagiError
|
|
20
|
+
? e.status
|
|
21
|
+
: this.context.abort.aborted
|
|
22
|
+
? "cancelled"
|
|
23
|
+
: "failed";
|
|
24
|
+
const text = await createContent.call(this, { error, status });
|
|
25
|
+
await writeFile(join(this.state.output, "report.md"), `${text}\n`);
|
|
26
|
+
await this.updateState({
|
|
27
|
+
completedAt: new Date().toISOString(),
|
|
28
|
+
status,
|
|
29
|
+
});
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function createContent(input) {
|
|
34
|
+
return filterEmpty([
|
|
35
|
+
...(await createMetaContent.call(this, input)),
|
|
36
|
+
...createCheckContent.call(this),
|
|
37
|
+
...createReviewerContent.call(this),
|
|
38
|
+
]).join("\n");
|
|
39
|
+
}
|
|
40
|
+
export async function createMetaContent(input) {
|
|
41
|
+
const events = await this.getEvents();
|
|
42
|
+
const lastAction = events.at(-1)?.message;
|
|
43
|
+
const rows = [
|
|
44
|
+
`- **Pull Request:** [${this.state.pr.number}](${this.state.pr.url})`,
|
|
45
|
+
`- **Mode**: ${toTitleCase(this.config.mode)}`,
|
|
46
|
+
`- **Dry run**: ${this.state.dryRun ? "Yes" : "No"}`,
|
|
47
|
+
`- **Status**: ${toTitleCase(input.status)}`,
|
|
48
|
+
`- **Automation**: ${this.state.pr?.automation ? toTitleCase(this.state.pr.automation.toLocaleLowerCase()) : "none"}`,
|
|
49
|
+
];
|
|
50
|
+
if (this.state.pr?.verdict)
|
|
51
|
+
rows.push(`- **Verdict**: ${toTitleCase(this.state.pr.verdict.toLocaleLowerCase())}`);
|
|
52
|
+
if (lastAction)
|
|
53
|
+
rows.push(`- **Last action**: ${lastAction}`);
|
|
54
|
+
if (input.error)
|
|
55
|
+
rows.push(`- **Error**: ${input.error}`);
|
|
56
|
+
return rows;
|
|
57
|
+
}
|
|
58
|
+
export function createCheckContent() {
|
|
59
|
+
const checks = this.state.pr?.checks;
|
|
60
|
+
if (!checks)
|
|
61
|
+
return [];
|
|
62
|
+
const failures = [
|
|
63
|
+
...checks.failed.map((check) => ({
|
|
64
|
+
comments: Object.entries(check.classifieds ?? {}).map(([reviewer, { comment, scope }]) => ({ comment, reviewer, scope })),
|
|
65
|
+
detail: check.scope == null
|
|
66
|
+
? "Failed"
|
|
67
|
+
: check.scope
|
|
68
|
+
? "In-scope failure"
|
|
69
|
+
: "Out-of-scope failure",
|
|
70
|
+
name: check.name,
|
|
71
|
+
})),
|
|
72
|
+
...checks.pending.map((check) => ({
|
|
73
|
+
comments: [],
|
|
74
|
+
detail: "Pending",
|
|
75
|
+
name: check.name,
|
|
76
|
+
})),
|
|
77
|
+
];
|
|
78
|
+
if (failures.length)
|
|
79
|
+
return [
|
|
80
|
+
"- **Check**: Failure",
|
|
81
|
+
...failures.flatMap(({ comments, detail, name }) => [
|
|
82
|
+
` - **${name}**: ${detail}`,
|
|
83
|
+
...comments.map(({ comment, reviewer, scope }) => ` - **${reviewer}**: ${scope ? "In scope" : "Out of scope"}. ${comment}`),
|
|
84
|
+
]),
|
|
85
|
+
];
|
|
86
|
+
else
|
|
87
|
+
return ["- **Check**: Pass"];
|
|
88
|
+
}
|
|
89
|
+
export function createReviewerContent() {
|
|
90
|
+
const reviewers = Object.entries(this.state.reviewers ?? {});
|
|
91
|
+
if (!reviewers.length)
|
|
92
|
+
return [];
|
|
93
|
+
return [
|
|
94
|
+
[
|
|
95
|
+
"- **Reviewer**:",
|
|
96
|
+
...reviewers.flatMap(([id, { outputs, posted, review }]) => {
|
|
97
|
+
if (!outputs?.length)
|
|
98
|
+
return [];
|
|
99
|
+
const output = outputs.at(-1);
|
|
100
|
+
const url = posted ?? review?.html_url;
|
|
101
|
+
const status = toTitleCase(output.verdict.toLocaleLowerCase());
|
|
102
|
+
const prevStatuses = outputs
|
|
103
|
+
.slice(0, -1)
|
|
104
|
+
.map(({ verdict }) => toTitleCase(verdict.toLocaleLowerCase()))
|
|
105
|
+
.reduce((prev, current) => {
|
|
106
|
+
if (prev.at(-1) !== current)
|
|
107
|
+
prev.push(current);
|
|
108
|
+
return prev;
|
|
109
|
+
}, []);
|
|
110
|
+
const lines = [
|
|
111
|
+
` - **${id}**: ${[...prevStatuses, url ? `[${status}](${url})` : status].join(" -> ")}`,
|
|
112
|
+
...outputs.flatMap((output, index) => {
|
|
113
|
+
const lines = [
|
|
114
|
+
` - **Verdict**: ${toTitleCase(output.verdict.toLocaleLowerCase())}`,
|
|
115
|
+
];
|
|
116
|
+
const latest = index === outputs.length - 1;
|
|
117
|
+
const findings = output.findings ?? output.newFindings ?? [];
|
|
118
|
+
if (output.verdict === "CLOSED")
|
|
119
|
+
lines.push(` - **Comment**: ${latest ? "" : "~~"}${output.comment ?? review?.body}${latest ? "" : "~~"}`);
|
|
120
|
+
if (findings.length) {
|
|
121
|
+
lines.push(" - **Findings**:");
|
|
122
|
+
for (const { body, line, path, startLine, state } of findings) {
|
|
123
|
+
const discarded = state === "discarded";
|
|
124
|
+
const prefix = `${path}:${startLine != null ? `${startLine}-` : ""}${line}`;
|
|
125
|
+
lines.push(` - ${discarded ? "~~" : ""}\`${prefix}\`: ${body}${discarded ? "~~" : ""}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (output.followUps?.length) {
|
|
129
|
+
lines.push(" - **FollowUps**:");
|
|
130
|
+
for (const { body, commentId } of output.followUps) {
|
|
131
|
+
const thread = this.state.pr?.threads?.find(({ comments }) => comments.some(({ databaseId }) => databaseId === commentId));
|
|
132
|
+
const prefix = `${thread?.path ?? "unknown"}:${thread?.line ?? "N/A"}`;
|
|
133
|
+
lines.push(` - \`${prefix}\`: ${body}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return lines;
|
|
137
|
+
}),
|
|
138
|
+
];
|
|
139
|
+
return lines;
|
|
140
|
+
}),
|
|
141
|
+
].join("\n"),
|
|
142
|
+
];
|
|
143
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { MagiError } from "@/magi";
|
|
3
|
+
import { createExecWithGitHubApiRetry, quote } from "@/utils";
|
|
4
|
+
import { automate, isAutomationEnabled, postReviews } from "./action";
|
|
5
|
+
import { checkCi, checkPr, classifyChecks, rerunChecks } from "./check";
|
|
6
|
+
import { checkExistingReviews, fetchReviewContext } from "./context";
|
|
7
|
+
import { createReport } from "./report";
|
|
8
|
+
import { reconsiderClose, review, validateFindings } from "./reviewer";
|
|
9
|
+
export class Review {
|
|
10
|
+
number;
|
|
11
|
+
magi;
|
|
12
|
+
config;
|
|
13
|
+
context;
|
|
14
|
+
octokit;
|
|
15
|
+
graphql;
|
|
16
|
+
exec;
|
|
17
|
+
state;
|
|
18
|
+
constructor(number, magi, config, context, octokit, graphql, exec, state) {
|
|
19
|
+
this.number = number;
|
|
20
|
+
this.magi = magi;
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.context = context;
|
|
23
|
+
this.octokit = octokit;
|
|
24
|
+
this.graphql = graphql;
|
|
25
|
+
this.exec = exec;
|
|
26
|
+
this.exec = createExecWithGitHubApiRetry(this.magi.exec, this.config.github.retryApiAttempts);
|
|
27
|
+
this.state = state;
|
|
28
|
+
}
|
|
29
|
+
static async init(number, magi, config, context, options) {
|
|
30
|
+
const { exec, graphql, octokit, ...rest } = await this.setup(number, magi, config, context);
|
|
31
|
+
const state = await magi.createState(join(config.review.output, number.toString()), { ...options, ...rest, command: "review" });
|
|
32
|
+
await magi.updateEvent(state.output, `Started reviewing.`);
|
|
33
|
+
return new Review(number, magi, config, context, octokit, graphql, exec, state);
|
|
34
|
+
}
|
|
35
|
+
static async setup(number, magi, config, context) {
|
|
36
|
+
const url = `${config.github.url}/pull/${number}`;
|
|
37
|
+
const octokit = await magi.createOctokit(config, context.abort);
|
|
38
|
+
const graphql = magi.createGraphql(octokit);
|
|
39
|
+
const reviewers = Object.fromEntries(config.review.reviewers.map(({ account, id, model, permissions }) => [id, { account, model, permissions }]));
|
|
40
|
+
const operator = config.review.operator
|
|
41
|
+
? config.review.reviewers.find(({ id }) => id === config.review.operator)
|
|
42
|
+
: config.review.reviewers[Math.abs(number) % config.review.reviewers.length];
|
|
43
|
+
const exec = createExecWithGitHubApiRetry(magi.exec, config.github.retryApiAttempts);
|
|
44
|
+
return {
|
|
45
|
+
exec,
|
|
46
|
+
graphql,
|
|
47
|
+
octokit,
|
|
48
|
+
operator,
|
|
49
|
+
pr: { number, url },
|
|
50
|
+
repo: quote(`${config.github.owner}/${config.github.repo}`),
|
|
51
|
+
reviewers,
|
|
52
|
+
sessionId: context.sessionID,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
checkPr = checkPr;
|
|
56
|
+
checkExistingReviews = checkExistingReviews;
|
|
57
|
+
checkCi = checkCi;
|
|
58
|
+
classifyChecks = classifyChecks;
|
|
59
|
+
rerunChecks = rerunChecks;
|
|
60
|
+
fetchReviewContext = fetchReviewContext;
|
|
61
|
+
review = review;
|
|
62
|
+
validateFindings = validateFindings;
|
|
63
|
+
reconsiderClose = reconsiderClose;
|
|
64
|
+
postReviews = postReviews;
|
|
65
|
+
isAutomationEnabled = isAutomationEnabled;
|
|
66
|
+
automate = automate;
|
|
67
|
+
createReport = createReport;
|
|
68
|
+
async cleanup() {
|
|
69
|
+
if (this.context.abort.aborted &&
|
|
70
|
+
["preparing", "running"].includes(this.state.status))
|
|
71
|
+
await this.updateState({
|
|
72
|
+
completedAt: new Date().toISOString(),
|
|
73
|
+
status: "cancelled",
|
|
74
|
+
});
|
|
75
|
+
if (this.state.worktree?.path)
|
|
76
|
+
await this.magi.deleteWorktree(this.state.worktree.path);
|
|
77
|
+
}
|
|
78
|
+
async updateState(next) {
|
|
79
|
+
this.state = await this.magi.updateState(this.state.output, next);
|
|
80
|
+
}
|
|
81
|
+
async updateEvent(message) {
|
|
82
|
+
await this.magi.updateEvent(this.state.output, message);
|
|
83
|
+
}
|
|
84
|
+
async getEvents() {
|
|
85
|
+
return await this.magi.getEvents(this.state.output);
|
|
86
|
+
}
|
|
87
|
+
async createAgentFile(phase, id, content, attempt, cycle) {
|
|
88
|
+
await this.magi.createAgentFile(this.state.output, phase, id, content, attempt, cycle);
|
|
89
|
+
}
|
|
90
|
+
async createSessions() {
|
|
91
|
+
this.context.abort.throwIfAborted();
|
|
92
|
+
if (!this.state.reviewers)
|
|
93
|
+
throw new MagiError("blocked", "Reviewers not found.");
|
|
94
|
+
if (!this.state.operator)
|
|
95
|
+
throw new MagiError("blocked", "Operator not found.");
|
|
96
|
+
const reviewers = Object.fromEntries(await Promise.all(Object.entries(this.state.reviewers).map(async ([id, { model, permissions }]) => [
|
|
97
|
+
id,
|
|
98
|
+
{
|
|
99
|
+
sessionId: await this.magi.createSession(this.state.sessionId, `magi review #${this.number} ${id}`, { model, permissions }, this.context.abort),
|
|
100
|
+
},
|
|
101
|
+
])));
|
|
102
|
+
const operator = {
|
|
103
|
+
sessionId: await this.magi.createSession(this.state.sessionId, `magi review #${this.number} operator`, {
|
|
104
|
+
model: this.state.operator.model,
|
|
105
|
+
permissions: this.state.operator.permissions,
|
|
106
|
+
}, this.context.abort),
|
|
107
|
+
};
|
|
108
|
+
await this.updateState({ operator, reviewers });
|
|
109
|
+
}
|
|
110
|
+
async createWorktree() {
|
|
111
|
+
this.context.abort.throwIfAborted();
|
|
112
|
+
await this.updateEvent(`Creating worktree.`);
|
|
113
|
+
const worktree = await this.magi.createWorktree(this.config.review.worktree, this.number, this.state.id, this.context.abort);
|
|
114
|
+
await this.updateState({ worktree });
|
|
115
|
+
await this.updateEvent(`Finished creating worktree.`);
|
|
116
|
+
}
|
|
117
|
+
async resolveVerdict() {
|
|
118
|
+
if (!this.config.review.reviewers?.length)
|
|
119
|
+
throw new MagiError("blocked", "No reviewers configured.");
|
|
120
|
+
if (!this.state.reviewers)
|
|
121
|
+
throw new MagiError("blocked", "Reviewers not found.");
|
|
122
|
+
const counts = Object.entries(this.state.reviewers).reduce((prev, [id, { outputs }]) => {
|
|
123
|
+
const output = outputs?.at(-1);
|
|
124
|
+
if (!output)
|
|
125
|
+
throw new MagiError("blocked", `No output found for reviewer ${id}.`);
|
|
126
|
+
prev[output.verdict] += 1;
|
|
127
|
+
return prev;
|
|
128
|
+
}, {
|
|
129
|
+
APPROVED: 0,
|
|
130
|
+
CHANGES_REQUESTED: 0,
|
|
131
|
+
CLOSED: 0,
|
|
132
|
+
});
|
|
133
|
+
const length = this.config.review.reviewers.length;
|
|
134
|
+
const threshold = Math.floor(length / 2) + 1;
|
|
135
|
+
const majority = this.config.review.merge.approvalPolicy === "majority";
|
|
136
|
+
const verdict = counts.CLOSED >= threshold
|
|
137
|
+
? "CLOSED"
|
|
138
|
+
: (!majority && counts.APPROVED === length) ||
|
|
139
|
+
(majority && counts.APPROVED >= threshold)
|
|
140
|
+
? "APPROVED"
|
|
141
|
+
: "CHANGES_REQUESTED";
|
|
142
|
+
await this.updateState({ pr: { verdict } });
|
|
143
|
+
await this.updateEvent(`Final verdict is ${verdict}.`);
|
|
144
|
+
return verdict;
|
|
145
|
+
}
|
|
146
|
+
}
|