kodingo-cli 1.0.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.md +91 -0
- package/dist/adapters/agent-adapter/agent-connector.js +1 -0
- package/dist/adapters/ai/inference.js +55 -0
- package/dist/adapters/cloud-adapter/cloud-persistence.js +290 -0
- package/dist/adapters/db-adapter/memory-persistence.js +521 -0
- package/dist/adapters/git-adapter/git-listener.js +188 -0
- package/dist/cli.js +181 -0
- package/dist/commands/add-decision.js +1 -0
- package/dist/commands/affirm.js +41 -0
- package/dist/commands/canonicalize-symbol.js +95 -0
- package/dist/commands/capture.js +67 -0
- package/dist/commands/deny.js +67 -0
- package/dist/commands/doctor.js +168 -0
- package/dist/commands/explain-symbol.js +84 -0
- package/dist/commands/ignore.js +19 -0
- package/dist/commands/index.js +1 -0
- package/dist/commands/init.js +135 -0
- package/dist/commands/install-hook.js +61 -0
- package/dist/commands/query-memory.js +61 -0
- package/dist/commands/query-symbol.js +63 -0
- package/dist/commands/scan-git.js +59 -0
- package/dist/commands/uninstall-hook.js +51 -0
- package/dist/ports/cloud-event-port.js +207 -0
- package/dist/ports/db-event-port.js +195 -0
- package/dist/utils/persistence-config.js +69 -0
- package/dist/utils/repo-scope.js +48 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# kodingo-cli
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
This repository contains the Kodingo command-line interface.
|
|
6
|
+
|
|
7
|
+
The CLI acts as a local adapter between a developer’s environment and the Kodingo core engine.
|
|
8
|
+
|
|
9
|
+
It is responsible for observing, normalizing, and forwarding signals —
|
|
10
|
+
not interpreting them.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What This Repo Owns
|
|
15
|
+
|
|
16
|
+
- CLI commands and UX
|
|
17
|
+
- Local project initialization
|
|
18
|
+
- Project scoping and isolation
|
|
19
|
+
- Authentication and authorization flow
|
|
20
|
+
- Event emission to kodingo-core
|
|
21
|
+
- Local configuration and credentials
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## What This Repo Does NOT Do
|
|
26
|
+
|
|
27
|
+
- No decision inference
|
|
28
|
+
- No memory interpretation
|
|
29
|
+
- No confidence calculation
|
|
30
|
+
- No domain logic
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Design Constraints
|
|
35
|
+
|
|
36
|
+
- The CLI must be safe to run locally
|
|
37
|
+
- It must never leak data across projects
|
|
38
|
+
- It must respect paid vs unpaid project boundaries
|
|
39
|
+
- All intelligence lives elsewhere
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Dependencies
|
|
44
|
+
|
|
45
|
+
- Depends on `kodingo-core`
|
|
46
|
+
- Depends on `kodingo-schema`
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Security Model
|
|
51
|
+
|
|
52
|
+
- Projects are explicitly initialized
|
|
53
|
+
- Access is opt-in per project
|
|
54
|
+
- Local identity is scoped per project directory
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Status
|
|
59
|
+
|
|
60
|
+
Interface design phase.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Local DB Setup
|
|
65
|
+
|
|
66
|
+
### 1) Run Postgres locally
|
|
67
|
+
|
|
68
|
+
Use Docker:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
docker run --name kodingo-postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=kodingo -p 5432:5432 -d postgres:16
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Ensure `psql` is available in your PATH (it ships with Postgres client tools).
|
|
75
|
+
|
|
76
|
+
### 2) Configure environment
|
|
77
|
+
|
|
78
|
+
Create a `.env` file (or export in shell):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
export DATABASE_URL=postgres://postgres:postgres@localhost:5432/kodingo
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3) Build and run the CLI
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm ci
|
|
88
|
+
npm run build
|
|
89
|
+
node src/cli.js capture --type decision --title "Chosen DB" --content "We chose Postgres for persistence." --tags "architecture,db"
|
|
90
|
+
node src/cli.js query-memory "Postgres" --limit 5
|
|
91
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.inferDecisionSummary = inferDecisionSummary;
|
|
7
|
+
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
8
|
+
async function inferDecisionSummary(input) {
|
|
9
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
return buildFallback(input);
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const client = new sdk_1.default({ apiKey });
|
|
15
|
+
const prompt = [
|
|
16
|
+
"You are a senior software engineer reviewing a code change.",
|
|
17
|
+
"Your job is to interpret what this change represents as a decision or evolution in the codebase.",
|
|
18
|
+
"",
|
|
19
|
+
"Write 2 to 4 sentences of plain prose. No markdown, no bullet points, no headers.",
|
|
20
|
+
"Focus on intent and reasoning, not on describing the diff mechanically.",
|
|
21
|
+
"Treat the commit message as a low-trust hint only, not as truth.",
|
|
22
|
+
"Use the symbol and file paths to understand what part of the system changed.",
|
|
23
|
+
"If intent cannot be confidently inferred, say so honestly in plain English.",
|
|
24
|
+
"",
|
|
25
|
+
`Repository: ${input.repoName}`,
|
|
26
|
+
`Symbol: ${input.symbol}`,
|
|
27
|
+
`Changed files: ${input.changedFiles.join(", ")}`,
|
|
28
|
+
`Commit message (low-trust hint): ${input.commitMessage}`,
|
|
29
|
+
"",
|
|
30
|
+
"Diff:",
|
|
31
|
+
input.diff.slice(0, 8000),
|
|
32
|
+
].join("\n");
|
|
33
|
+
const response = await client.messages.create({
|
|
34
|
+
model: "claude-opus-4-5",
|
|
35
|
+
max_tokens: 300,
|
|
36
|
+
messages: [{ role: "user", content: prompt }],
|
|
37
|
+
});
|
|
38
|
+
const textBlock = response.content.find((b) => b.type === "text");
|
|
39
|
+
if (textBlock && textBlock.type === "text") {
|
|
40
|
+
return textBlock.text.trim();
|
|
41
|
+
}
|
|
42
|
+
return buildFallback(input);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return buildFallback(input);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function buildFallback(input) {
|
|
49
|
+
const shortHash = extractShortHash(input.diff);
|
|
50
|
+
return `Changes to ${input.symbol} across ${input.changedFiles.length} file(s)${shortHash ? ` in commit ${shortHash}` : ""}. Commit message: ${input.commitMessage}.`;
|
|
51
|
+
}
|
|
52
|
+
function extractShortHash(diff) {
|
|
53
|
+
const match = diff.match(/^commit ([a-f0-9]{7,})/m);
|
|
54
|
+
return match ? match[1].slice(0, 8) : "";
|
|
55
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cloud persistence adapter for kodingo-cli.
|
|
4
|
+
* Drop-in replacement for memory-persistence.ts — same function signatures,
|
|
5
|
+
* same return types, but talks to kodingo-api over HTTP instead of psql.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.initDb = initDb;
|
|
9
|
+
exports.checkDatabaseConnection = checkDatabaseConnection;
|
|
10
|
+
exports.saveMemory = saveMemory;
|
|
11
|
+
exports.getMemoryById = getMemoryById;
|
|
12
|
+
exports.updateMemoryLifecycle = updateMemoryLifecycle;
|
|
13
|
+
exports.updateMemoryConfidence = updateMemoryConfidence;
|
|
14
|
+
exports.applySignalToMemory = applySignalToMemory;
|
|
15
|
+
exports.suppressProposedSiblings = suppressProposedSiblings;
|
|
16
|
+
exports.getMemoryByExternalId = getMemoryByExternalId;
|
|
17
|
+
exports.getProposedOrIgnoredRecordBySymbol = getProposedOrIgnoredRecordBySymbol;
|
|
18
|
+
exports.deleteMemoryById = deleteMemoryById;
|
|
19
|
+
exports.queryMemory = queryMemory;
|
|
20
|
+
exports.queryMemoryBySymbol = queryMemoryBySymbol;
|
|
21
|
+
function getConfig() {
|
|
22
|
+
const apiUrl = process.env.KODINGO_API_URL;
|
|
23
|
+
const token = process.env.KODINGO_API_TOKEN;
|
|
24
|
+
if (!apiUrl)
|
|
25
|
+
throw new Error("KODINGO_API_URL is not set");
|
|
26
|
+
if (!token)
|
|
27
|
+
throw new Error("KODINGO_API_TOKEN is not set");
|
|
28
|
+
return { apiUrl: apiUrl.replace(/\/$/, ""), token };
|
|
29
|
+
}
|
|
30
|
+
// ── HTTP client ───────────────────────────────────────────────────────────────
|
|
31
|
+
async function apiFetch(path, options = {}) {
|
|
32
|
+
const { apiUrl, token } = getConfig();
|
|
33
|
+
const res = await fetch(`${apiUrl}${path}`, {
|
|
34
|
+
...options,
|
|
35
|
+
headers: {
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
"X-Kodingo-Token": token,
|
|
38
|
+
...(options.headers ?? {}),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const body = await res.text();
|
|
43
|
+
throw new Error(`kodingo-api error ${res.status}: ${body}`);
|
|
44
|
+
}
|
|
45
|
+
// 204 No Content
|
|
46
|
+
if (res.status === 204)
|
|
47
|
+
return undefined;
|
|
48
|
+
return res.json();
|
|
49
|
+
}
|
|
50
|
+
// ── Response → AdapterMemoryRecord mapper ─────────────────────────────────────
|
|
51
|
+
// The API returns camelCase JSON. Dates arrive as strings and are converted.
|
|
52
|
+
// Adapter-layer fields (externalId, correctsId, correctedById) are passed
|
|
53
|
+
// through so commands can display them without reaching back to the API.
|
|
54
|
+
function mapApiRecord(r) {
|
|
55
|
+
const record = {
|
|
56
|
+
id: r.id,
|
|
57
|
+
projectId: r.projectId,
|
|
58
|
+
repo: r.repo ?? "",
|
|
59
|
+
type: r.type,
|
|
60
|
+
content: r.content,
|
|
61
|
+
tags: r.tags ?? [],
|
|
62
|
+
status: r.status,
|
|
63
|
+
confidence: r.confidence,
|
|
64
|
+
evidence: {
|
|
65
|
+
strongCodeChangeCount: r.evidence?.strongCodeChangeCount ?? 0,
|
|
66
|
+
structuralCount: r.evidence?.structuralCount ?? 0,
|
|
67
|
+
metadataCount: r.evidence?.metadataCount ?? 0,
|
|
68
|
+
evidenceGain: r.evidence?.evidenceGain ?? 0,
|
|
69
|
+
},
|
|
70
|
+
createdAt: new Date(r.createdAt),
|
|
71
|
+
updatedAt: new Date(r.updatedAt),
|
|
72
|
+
// Adapter-layer fields — not on core MemoryRecord but present in API responses
|
|
73
|
+
externalId: r.externalId ?? null,
|
|
74
|
+
correctsId: r.correctsId ?? null,
|
|
75
|
+
correctedById: r.correctedById ?? null,
|
|
76
|
+
};
|
|
77
|
+
// Optional core fields — only set if present (exactOptionalPropertyTypes)
|
|
78
|
+
if (r.title)
|
|
79
|
+
record.title = r.title;
|
|
80
|
+
if (r.symbol)
|
|
81
|
+
record.symbol = r.symbol;
|
|
82
|
+
if (r.fingerprint)
|
|
83
|
+
record.fingerprint = r.fingerprint;
|
|
84
|
+
if (r.deniedAt)
|
|
85
|
+
record.deniedAt = new Date(r.deniedAt);
|
|
86
|
+
return record;
|
|
87
|
+
}
|
|
88
|
+
// ── Public API (mirrors memory-persistence.ts) ────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* No-op for cloud mode — schema is managed by kodingo-api.
|
|
91
|
+
*/
|
|
92
|
+
async function initDb() {
|
|
93
|
+
// Cloud: schema is managed server-side, nothing to do locally.
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Ping the API health endpoint to verify connectivity.
|
|
97
|
+
*/
|
|
98
|
+
async function checkDatabaseConnection() {
|
|
99
|
+
const { apiUrl } = getConfig();
|
|
100
|
+
const res = await fetch(`${apiUrl}/health`);
|
|
101
|
+
if (!res.ok)
|
|
102
|
+
throw new Error(`kodingo-api health check failed: ${res.status}`);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Save a new memory record.
|
|
106
|
+
*/
|
|
107
|
+
async function saveMemory(input) {
|
|
108
|
+
const body = {
|
|
109
|
+
type: input.type,
|
|
110
|
+
content: input.content,
|
|
111
|
+
status: input.status ?? "proposed",
|
|
112
|
+
confidence: input.confidence ?? 0.3,
|
|
113
|
+
tags: input.tags ?? [],
|
|
114
|
+
};
|
|
115
|
+
if (input.title)
|
|
116
|
+
body.title = input.title;
|
|
117
|
+
if (input.repoPath)
|
|
118
|
+
body.repo = input.repoPath;
|
|
119
|
+
if (input.symbol)
|
|
120
|
+
body.symbol = input.symbol;
|
|
121
|
+
if (input.externalId)
|
|
122
|
+
body.externalId = input.externalId;
|
|
123
|
+
if (input.correctsId)
|
|
124
|
+
body.correctsId = input.correctsId;
|
|
125
|
+
if (input.correctedById)
|
|
126
|
+
body.correctedById = input.correctedById;
|
|
127
|
+
const record = await apiFetch("/memory", {
|
|
128
|
+
method: "POST",
|
|
129
|
+
body: JSON.stringify(body),
|
|
130
|
+
});
|
|
131
|
+
return mapApiRecord(record);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get a memory record by ID.
|
|
135
|
+
*/
|
|
136
|
+
async function getMemoryById(id) {
|
|
137
|
+
try {
|
|
138
|
+
const record = await apiFetch(`/memory/${id}`);
|
|
139
|
+
return mapApiRecord(record);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (err.message?.includes("404"))
|
|
143
|
+
return null;
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Update lifecycle fields (status, confidence, correctedById).
|
|
149
|
+
*/
|
|
150
|
+
async function updateMemoryLifecycle(params) {
|
|
151
|
+
const body = {
|
|
152
|
+
status: params.status,
|
|
153
|
+
confidence: params.confidence,
|
|
154
|
+
};
|
|
155
|
+
if (params.correctedById)
|
|
156
|
+
body.correctedById = params.correctedById;
|
|
157
|
+
const record = await apiFetch(`/memory/${params.id}`, {
|
|
158
|
+
method: "PATCH",
|
|
159
|
+
body: JSON.stringify(body),
|
|
160
|
+
});
|
|
161
|
+
return mapApiRecord(record);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Update only the confidence of a memory record (signal accumulation).
|
|
165
|
+
*/
|
|
166
|
+
async function updateMemoryConfidence(params) {
|
|
167
|
+
const record = await apiFetch(`/memory/${params.id}/confidence`, {
|
|
168
|
+
method: "PATCH",
|
|
169
|
+
body: JSON.stringify({ confidence: params.confidence }),
|
|
170
|
+
});
|
|
171
|
+
return mapApiRecord(record);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Apply a signal weight class to a memory record (uses applySignal domain rules server-side).
|
|
175
|
+
*/
|
|
176
|
+
async function applySignalToMemory(params) {
|
|
177
|
+
const record = await apiFetch(`/memory/${params.id}/evidence`, {
|
|
178
|
+
method: "PATCH",
|
|
179
|
+
body: JSON.stringify({ weightClass: params.weightClass }),
|
|
180
|
+
});
|
|
181
|
+
return mapApiRecord(record);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Suppress stale proposed siblings for a symbol after affirmation.
|
|
185
|
+
*/
|
|
186
|
+
async function suppressProposedSiblings(params) {
|
|
187
|
+
const body = {
|
|
188
|
+
symbol: params.symbol,
|
|
189
|
+
excludeId: params.excludeId,
|
|
190
|
+
};
|
|
191
|
+
if (params.repoPath)
|
|
192
|
+
body.repo = params.repoPath;
|
|
193
|
+
const res = await apiFetch("/memory/suppress-siblings", {
|
|
194
|
+
method: "POST",
|
|
195
|
+
body: JSON.stringify(body),
|
|
196
|
+
});
|
|
197
|
+
return res.suppressed;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Look up a memory record by externalId, scoped to a repo.
|
|
201
|
+
*/
|
|
202
|
+
async function getMemoryByExternalId(externalId, repoPath) {
|
|
203
|
+
const params = new URLSearchParams({ externalId, repo: repoPath });
|
|
204
|
+
const res = await apiFetch(`/memory?${params.toString()}`);
|
|
205
|
+
const record = res.data?.[0];
|
|
206
|
+
if (!record)
|
|
207
|
+
return null;
|
|
208
|
+
return mapApiRecord(record);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Find the best proposed or ignored record for a symbol in a repo.
|
|
212
|
+
*/
|
|
213
|
+
async function getProposedOrIgnoredRecordBySymbol(params) {
|
|
214
|
+
const qs = new URLSearchParams({
|
|
215
|
+
symbol: params.symbol,
|
|
216
|
+
repo: params.repoPath,
|
|
217
|
+
});
|
|
218
|
+
const [proposed, ignored] = await Promise.all([
|
|
219
|
+
apiFetch(`/memory?${qs}&status=proposed&limit=1`),
|
|
220
|
+
apiFetch(`/memory?${qs}&status=ignored&limit=1`),
|
|
221
|
+
]);
|
|
222
|
+
const candidates = [...(proposed.data ?? []), ...(ignored.data ?? [])].map(mapApiRecord);
|
|
223
|
+
if (candidates.length === 0)
|
|
224
|
+
return null;
|
|
225
|
+
return candidates.sort((a, b) => {
|
|
226
|
+
if (a.status === "proposed" && b.status !== "proposed")
|
|
227
|
+
return -1;
|
|
228
|
+
if (b.status === "proposed" && a.status !== "proposed")
|
|
229
|
+
return 1;
|
|
230
|
+
return b.confidence - a.confidence;
|
|
231
|
+
})[0];
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Delete a memory record by ID.
|
|
235
|
+
*/
|
|
236
|
+
async function deleteMemoryById(id) {
|
|
237
|
+
await apiFetch(`/memory/${id}`, { method: "DELETE" });
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Text query — full-text search via the API.
|
|
241
|
+
*/
|
|
242
|
+
async function queryMemory(text, limit = 10, repoPath, opts = {}) {
|
|
243
|
+
const qs = new URLSearchParams({ limit: String(limit) });
|
|
244
|
+
if (repoPath)
|
|
245
|
+
qs.set("repo", repoPath);
|
|
246
|
+
if (!opts.includeNonCanonical)
|
|
247
|
+
qs.set("status", "proposed");
|
|
248
|
+
// Use server-side FTS when text is provided
|
|
249
|
+
const term = text.trim();
|
|
250
|
+
if (term)
|
|
251
|
+
qs.set("q", term);
|
|
252
|
+
const res = await apiFetch(`/memory?${qs.toString()}`);
|
|
253
|
+
let records = (res.data ?? []).map(mapApiRecord);
|
|
254
|
+
if (opts.dedupeBySymbol) {
|
|
255
|
+
const seen = new Set();
|
|
256
|
+
records = records.filter((r) => {
|
|
257
|
+
const key = r.symbol ?? r.id;
|
|
258
|
+
if (seen.has(key))
|
|
259
|
+
return false;
|
|
260
|
+
seen.add(key);
|
|
261
|
+
return true;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return records.slice(0, limit);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Symbol query with canonical preference.
|
|
268
|
+
*/
|
|
269
|
+
async function queryMemoryBySymbol(params) {
|
|
270
|
+
const qs = new URLSearchParams({
|
|
271
|
+
symbol: params.symbol,
|
|
272
|
+
limit: String(params.limit ?? 10),
|
|
273
|
+
});
|
|
274
|
+
if (params.repoPath)
|
|
275
|
+
qs.set("repo", params.repoPath);
|
|
276
|
+
if (params.onlyCanonical)
|
|
277
|
+
qs.set("status", "affirmed");
|
|
278
|
+
const res = await apiFetch(`/memory?${qs.toString()}`);
|
|
279
|
+
let records = (res.data ?? []).map(mapApiRecord);
|
|
280
|
+
if (!params.includeDenied && !params.onlyCanonical) {
|
|
281
|
+
records = records.filter((r) => r.status !== "denied");
|
|
282
|
+
}
|
|
283
|
+
return records.sort((a, b) => {
|
|
284
|
+
if (a.status === "affirmed" && b.status !== "affirmed")
|
|
285
|
+
return -1;
|
|
286
|
+
if (b.status === "affirmed" && a.status !== "affirmed")
|
|
287
|
+
return 1;
|
|
288
|
+
return b.confidence - a.confidence;
|
|
289
|
+
});
|
|
290
|
+
}
|