memorix 1.6.3 → 1.7.1
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/CHANGELOG.md +31 -0
- package/dist/cli/index.js +1029 -643
- package/dist/cli/memcode.js +0 -0
- package/dist/dashboard/static/app.js +298 -37
- package/dist/dashboard/static/index.html +17 -0
- package/dist/dashboard/static/style.css +328 -0
- package/dist/index.js +902 -394
- package/dist/memcode-runtime/CHANGELOG.md +31 -0
- package/dist/sdk.js +901 -393
- package/docs/ARCHITECTURE.md +3 -0
- package/docs/DASHBOARD.md +54 -0
- package/docs/README.md +2 -0
- package/package.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/cleanup.ts +13 -114
- package/src/cli/commands/memory.ts +12 -43
- package/src/dashboard/maintenance.ts +303 -0
- package/src/dashboard/server.ts +100 -2
- package/src/dashboard/static/index.html +17 -0
- package/src/dashboard/static/style.css +328 -0
- package/src/memory/cleanup.ts +143 -0
- package/src/memory/deduplication.ts +178 -0
- package/src/server.ts +12 -46
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -497,8 +497,11 @@ It provides an operational view for:
|
|
|
497
497
|
- identity health
|
|
498
498
|
- sessions
|
|
499
499
|
- retention state
|
|
500
|
+
- preview-first cleanup, consolidation, intelligent deduplication, and retention archival
|
|
500
501
|
- read-only orchestration coordination state in the standalone dashboard and live state in HTTP service mode
|
|
501
502
|
|
|
503
|
+
Dashboard maintenance is not a separate business-logic implementation. CLI, MCP, background jobs, and Dashboard routes share the same cleanup, consolidation, deduplication, and retention functions. Mutation routes issue a project-bound signed preview token and reject execution when the candidate set changed after preview.
|
|
504
|
+
|
|
502
505
|
This is part of Memorix's shift from a single MCP server to a broader local memory platform.
|
|
503
506
|
|
|
504
507
|
---
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Dashboard
|
|
2
|
+
|
|
3
|
+
The Memorix Dashboard is the local control surface for inspecting project memory and running explicit maintenance. It is available through the shared background service or as a standalone local process.
|
|
4
|
+
|
|
5
|
+
## Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
memorix background start
|
|
9
|
+
# dashboard: http://127.0.0.1:3211
|
|
10
|
+
|
|
11
|
+
memorix dashboard
|
|
12
|
+
# standalone dashboard: http://127.0.0.1:3210
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Both modes read the same project-scoped stores. The project switcher changes the active project filter; it does not merge projects or grant access to personal memories.
|
|
16
|
+
|
|
17
|
+
## Maintenance Workbench
|
|
18
|
+
|
|
19
|
+
The Observations page exposes three explicit maintenance actions:
|
|
20
|
+
|
|
21
|
+
| Action | Effect | Shared implementation |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| Cleanup | Deletes low-quality activity records and exact duplicates; optionally archives test/demo noise | `analyzeCleanupObservations` and `applyCleanupMutations` |
|
|
24
|
+
| Consolidate | Merges strongly similar project-visible observations while preserving facts and references | `findConsolidationCandidates` and `executeConsolidation` |
|
|
25
|
+
| Deduplicate | Uses the configured memory LLM to identify redundant or superseded records and marks them resolved | `planMemoryDeduplication` and `applyDeduplicationPlan` |
|
|
26
|
+
|
|
27
|
+
The Retention page exposes Archive for records already classified as archive candidates by the canonical retention projection.
|
|
28
|
+
|
|
29
|
+
Every action follows the same contract:
|
|
30
|
+
|
|
31
|
+
1. The Dashboard requests a project-scoped preview.
|
|
32
|
+
2. The server returns the candidate records, counts, and a signed preview token.
|
|
33
|
+
3. The user confirms the displayed plan.
|
|
34
|
+
4. Execution verifies the token and current candidate set before mutating data.
|
|
35
|
+
5. If memory changed after preview, execution fails with `409` and requires a new preview.
|
|
36
|
+
|
|
37
|
+
Cleanup noise archival is opt-in. Consolidation never touches personal or team-scoped observations from an unbound Dashboard. Deduplication marks records resolved rather than deleting them. Retention changes status to archived. Manual selection and hard deletion remain separate from the maintenance workbench.
|
|
38
|
+
|
|
39
|
+
## HTTP Contract
|
|
40
|
+
|
|
41
|
+
Maintenance endpoints are local Dashboard APIs and are not a replacement for MCP or CLI integrations:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
POST /api/maintenance/cleanup/preview
|
|
45
|
+
POST /api/maintenance/cleanup/execute
|
|
46
|
+
POST /api/maintenance/consolidate/preview
|
|
47
|
+
POST /api/maintenance/consolidate/execute
|
|
48
|
+
POST /api/maintenance/deduplicate/preview
|
|
49
|
+
POST /api/maintenance/deduplicate/execute
|
|
50
|
+
POST /api/maintenance/retention/preview
|
|
51
|
+
POST /api/maintenance/retention/execute
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Use the CLI for scripts and remote terminals. Use MCP for agent-driven maintenance. The Dashboard is the reviewable local operator surface; all three call the same maintenance logic.
|
package/docs/README.md
CHANGED
|
@@ -33,6 +33,7 @@ The public docs are organized by user intent:
|
|
|
33
33
|
| CLI commands and MCP tools | [API_REFERENCE.md](API_REFERENCE.md) |
|
|
34
34
|
| Memory Autopilot, Code State, and context packs | [API_REFERENCE.md § Memory Autopilot, Code State, and Context Packs](API_REFERENCE.md#memory-autopilot-code-state-and-context-packs), [1.2 Code State](1.2.0-CODE-STATE.md), and [1.2 Workset Retrieval](1.2.0-WORKSET-RETRIEVAL.md) |
|
|
35
35
|
| Reviewed episodic, semantic, and procedural long-term memory | [1.3 Memory Architecture](1.3-MEMORY-ARCHITECTURE.md) |
|
|
36
|
+
| Browse memory and run preview-first local maintenance | [Dashboard](DASHBOARD.md) |
|
|
36
37
|
| Reviewable knowledge pages and workflow inheritance | [API_REFERENCE.md § Knowledge Workspace and Workflows](API_REFERENCE.md#knowledge-workspace-and-workflows), [1.2 Knowledge Workspace](1.2.0-KNOWLEDGE-WORKSPACE.md), and [1.2 Workflow Inheritance](1.2.0-WORKFLOW-INHERITANCE.md) |
|
|
37
38
|
| Git-derived engineering memory | [GIT_MEMORY.md](GIT_MEMORY.md) |
|
|
38
39
|
| Memory formation and quality pipeline | [MEMORY_FORMATION_PIPELINE.md](MEMORY_FORMATION_PIPELINE.md) |
|
|
@@ -51,6 +52,7 @@ The public docs are organized by user intent:
|
|
|
51
52
|
| TOML-first configuration | [CONFIGURATION.md](CONFIGURATION.md) |
|
|
52
53
|
| Docker/compose deployment | [DOCKER.md](DOCKER.md) |
|
|
53
54
|
| Resource and timeout tuning | [PERFORMANCE.md](PERFORMANCE.md) |
|
|
55
|
+
| Dashboard modes, maintenance actions, and safety contract | [DASHBOARD.md](DASHBOARD.md) |
|
|
54
56
|
| AI-facing install and troubleshooting playbook | [Agent Playbook](AGENT_OPERATOR_PLAYBOOK.md) |
|
|
55
57
|
|
|
56
58
|
---
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memorix",
|
|
3
3
|
"mcpName": "io.github.AVIDS2/memorix",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.7.1",
|
|
5
5
|
"description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
package/server.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"url": "https://github.com/AVIDS2/memorix",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.7.1",
|
|
11
11
|
"websiteUrl": "https://github.com/AVIDS2/memorix#readme",
|
|
12
12
|
"icons": [
|
|
13
13
|
{
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "memorix",
|
|
25
|
-
"version": "1.
|
|
25
|
+
"version": "1.7.1",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"packageArguments": [
|
|
28
28
|
{
|
|
@@ -17,88 +17,15 @@ import { defineCommand } from 'citty';
|
|
|
17
17
|
import type { Observation } from '../../types.js';
|
|
18
18
|
import { detectProject } from '../../project/detector.js';
|
|
19
19
|
import { getProjectDataDir } from '../../store/persistence.js';
|
|
20
|
-
import type { ObservationStore } from '../../store/obs-store.js';
|
|
21
20
|
import { getObservationStore, initObservationStore } from '../../store/obs-store.js';
|
|
22
21
|
import { filterReadableObservations } from '../../memory/visibility.js';
|
|
22
|
+
import {
|
|
23
|
+
analyzeCleanupObservations,
|
|
24
|
+
applyCleanupMutations,
|
|
25
|
+
isLowQualityObservation,
|
|
26
|
+
} from '../../memory/cleanup.js';
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
const LOW_QUALITY_PATTERNS = [
|
|
26
|
-
/^Session activity/i,
|
|
27
|
-
/^Updated \S+\.\w+$/i,
|
|
28
|
-
/^Created \S+\.\w+$/i,
|
|
29
|
-
/^Deleted \S+\.\w+$/i,
|
|
30
|
-
/^Modified \S+\.\w+$/i,
|
|
31
|
-
/^Ran command:/i,
|
|
32
|
-
/^Read file:/i,
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
/** Patterns for demo/test noise — matches session.ts NOISE_PATTERNS */
|
|
36
|
-
const NOISE_PATTERNS = [
|
|
37
|
-
/\[测试\]/i, /\[test\]/i, /验证/i, /兼容/i, /\bcompat(?:ibility)?\b/i,
|
|
38
|
-
/\bdemo\b/i, /展示/i, /全能力/i, /handoff/i, /交接/i,
|
|
39
|
-
/for_memmcp_test/i, /\bbenchmark\b/i, /\bsandbox\b/i, /\bplayground\b/i,
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
/** Patterns for Memorix system self-reference — should not pollute unrelated projects */
|
|
43
|
-
const SYSTEM_SELF_PATTERNS = [
|
|
44
|
-
/memorix.demo/i, /memorix.*全能力/i, /memorix.*工具.*能力/i,
|
|
45
|
-
/memorix.*runtime.*mode/i, /memorix.*运行模式/i, /memorix.*control.plane/i,
|
|
46
|
-
/session.*inject(?:ion)?/i, /注入.*逻辑/i,
|
|
47
|
-
/\b22\s*(?:个|tools?).*(?:工具|能力|capabilit)/i,
|
|
48
|
-
/memorix.*(?:v\d|版本|version)/i, /memorix.*(?:兼容|compat)/i,
|
|
49
|
-
/memorix.*(?:测试|test)/i, /memmcp/i,
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
/** Check if an observation title matches low-quality patterns */
|
|
53
|
-
function isLowQuality(title: string): boolean {
|
|
54
|
-
return LOW_QUALITY_PATTERNS.some(p => p.test(title.trim()));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Check if observation text matches noise/demo/test/system-self patterns */
|
|
58
|
-
function isNoisePollution(obs: { title?: string; narrative?: string; entityName?: string; facts?: string[]; concepts?: string[] }): { isNoise: boolean; reason: string } {
|
|
59
|
-
const text = [obs.title, obs.narrative, obs.entityName, ...(obs.facts ?? []), ...(obs.concepts ?? [])]
|
|
60
|
-
.filter(Boolean).join('\n');
|
|
61
|
-
for (const p of SYSTEM_SELF_PATTERNS) {
|
|
62
|
-
if (p.test(text)) return { isNoise: true, reason: 'system-self' };
|
|
63
|
-
}
|
|
64
|
-
for (const p of NOISE_PATTERNS) {
|
|
65
|
-
if (p.test(text)) return { isNoise: true, reason: 'demo/test/noise' };
|
|
66
|
-
}
|
|
67
|
-
return { isNoise: false, reason: '' };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function requireObservationIds(observations: Observation[], action: string): number[] {
|
|
71
|
-
const ids = observations.map((observation) => observation.id);
|
|
72
|
-
if (ids.some((id) => typeof id !== 'number')) {
|
|
73
|
-
throw new Error(`Cannot ${action}: an observation has no persisted ID.`);
|
|
74
|
-
}
|
|
75
|
-
return ids as number[];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Apply cleanup mutations without replacing the shared observation table.
|
|
80
|
-
* Keeping lifecycle updates targeted prevents a cleanup in one project from
|
|
81
|
-
* overwriting observations written concurrently by another project.
|
|
82
|
-
*/
|
|
83
|
-
export async function applyCleanupMutations(
|
|
84
|
-
store: ObservationStore,
|
|
85
|
-
toArchive: Observation[],
|
|
86
|
-
toRemove: Observation[],
|
|
87
|
-
): Promise<{ archived: number; removed: number }> {
|
|
88
|
-
const archiveIds = requireObservationIds(toArchive, 'archive');
|
|
89
|
-
const removeIds = requireObservationIds(toRemove, 'delete');
|
|
90
|
-
const removals = new Set(removeIds);
|
|
91
|
-
if (archiveIds.some((id) => removals.has(id))) {
|
|
92
|
-
throw new Error('Cleanup cannot archive and delete the same observation.');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
await store.atomic(async (tx) => {
|
|
96
|
-
await Promise.all(archiveIds.map((id) => tx.setStatus(id, 'archived')));
|
|
97
|
-
await Promise.all(removeIds.map((id) => tx.remove(id)));
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
return { archived: archiveIds.length, removed: removeIds.length };
|
|
101
|
-
}
|
|
28
|
+
export { applyCleanupMutations } from '../../memory/cleanup.js';
|
|
102
29
|
|
|
103
30
|
export default defineCommand({
|
|
104
31
|
meta: {
|
|
@@ -170,40 +97,12 @@ export default defineCommand({
|
|
|
170
97
|
return;
|
|
171
98
|
}
|
|
172
99
|
|
|
173
|
-
|
|
174
|
-
const lowQuality
|
|
175
|
-
const highQuality = projectObs.filter(o => !isLowQuality(o.title ?? ''));
|
|
176
|
-
|
|
177
|
-
// Find duplicates (same title + type + entity)
|
|
178
|
-
const seen = new Set<string>();
|
|
179
|
-
const duplicates: typeof projectObs = [];
|
|
180
|
-
const unique: typeof projectObs = [];
|
|
181
|
-
for (const obs of highQuality) {
|
|
182
|
-
const key = `${obs.type}|${obs.title}|${obs.entityName}`;
|
|
183
|
-
if (seen.has(key)) {
|
|
184
|
-
duplicates.push(obs);
|
|
185
|
-
} else {
|
|
186
|
-
seen.add(key);
|
|
187
|
-
unique.push(obs);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Find noise pollution (demo/test/system-self)
|
|
192
|
-
const noiseHits: Array<{ obs: typeof projectObs[0]; reason: string }> = [];
|
|
193
|
-
if (args.noise) {
|
|
194
|
-
for (const obs of projectObs) {
|
|
195
|
-
if (lowQuality.includes(obs) || duplicates.includes(obs)) continue;
|
|
196
|
-
const { isNoise, reason } = isNoisePollution(obs);
|
|
197
|
-
if (isNoise) noiseHits.push({ obs, reason });
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const toRemove = [...lowQuality, ...duplicates];
|
|
202
|
-
const toArchive = noiseHits.map(h => h.obs);
|
|
100
|
+
const analysis = analyzeCleanupObservations(projectObs as Observation[], { includeNoise: args.noise });
|
|
101
|
+
const { lowQuality, duplicates, noise: noiseHits, toRemove, toArchive } = analysis;
|
|
203
102
|
|
|
204
103
|
console.log(`Analysis (active observations for ${projectId}):`);
|
|
205
|
-
console.log(` Total active: ${
|
|
206
|
-
console.log(` High quality: ${
|
|
104
|
+
console.log(` Total active: ${analysis.totalActive}`);
|
|
105
|
+
console.log(` High quality: ${analysis.highQuality}`);
|
|
207
106
|
console.log(` Low quality: ${lowQuality.length}`);
|
|
208
107
|
console.log(` Duplicates: ${duplicates.length}`);
|
|
209
108
|
if (args.noise) {
|
|
@@ -224,7 +123,7 @@ export default defineCommand({
|
|
|
224
123
|
if (toRemove.length > 0) {
|
|
225
124
|
console.log('Items to DELETE:');
|
|
226
125
|
toRemove.slice(0, 10).forEach(o => {
|
|
227
|
-
const tag =
|
|
126
|
+
const tag = isLowQualityObservation(o.title ?? '') ? '(low-quality)' : '(duplicate)';
|
|
228
127
|
console.log(` ${tag} #${o.id ?? '?'} "${o.title}" [${o.type}]`);
|
|
229
128
|
});
|
|
230
129
|
if (toRemove.length > 10) {
|
|
@@ -236,8 +135,8 @@ export default defineCommand({
|
|
|
236
135
|
// Preview noise archival
|
|
237
136
|
if (toArchive.length > 0) {
|
|
238
137
|
console.log('Items to ARCHIVE (noise pollution):');
|
|
239
|
-
noiseHits.slice(0, 15).forEach(({
|
|
240
|
-
console.log(` (${reason}) #${
|
|
138
|
+
noiseHits.slice(0, 15).forEach(({ observation, reason }) => {
|
|
139
|
+
console.log(` (${reason}) #${observation.id ?? '?'} "${observation.title}" [${observation.type}] entity=${observation.entityName}`);
|
|
241
140
|
});
|
|
242
141
|
if (toArchive.length > 15) {
|
|
243
142
|
console.log(` ... and ${toArchive.length - 15} more`);
|
|
@@ -3,7 +3,9 @@ import { compactDetail, compactSearch, compactTimeline } from '../../compact/eng
|
|
|
3
3
|
import { withFreshIndex } from '../../memory/freshness.js';
|
|
4
4
|
import { getAllObservations, getObservation, getProjectObservations, resolveObservations, storeObservation, suggestTopicKey } from '../../memory/observations.js';
|
|
5
5
|
import { buildGraphContextPacket, formatGraphContextPrompt } from '../../memory/graph-context.js';
|
|
6
|
+
import { applyDeduplicationPlan, planMemoryDeduplication } from '../../memory/deduplication.js';
|
|
6
7
|
import { canManageObservation, filterReadableObservations, resolveObservationVisibility } from '../../memory/visibility.js';
|
|
8
|
+
import { getObservationStore } from '../../store/obs-store.js';
|
|
7
9
|
import {
|
|
8
10
|
asStringArg,
|
|
9
11
|
coerceObservationStatus,
|
|
@@ -288,12 +290,11 @@ export default defineCommand({
|
|
|
288
290
|
return;
|
|
289
291
|
}
|
|
290
292
|
|
|
291
|
-
const { deduplicateMemory } = await import('../../llm/memory-manager.js');
|
|
292
293
|
const allObs = await withFreshIndex(() =>
|
|
293
294
|
filterReadableObservations(
|
|
294
295
|
getAllObservations().filter((obs) => (obs.status ?? 'active') === 'active' && obs.projectId === project.id),
|
|
295
296
|
reader,
|
|
296
|
-
),
|
|
297
|
+
).filter((observation) => canManageObservation(observation, reader)),
|
|
297
298
|
);
|
|
298
299
|
|
|
299
300
|
if (allObs.length < 2) {
|
|
@@ -310,55 +311,23 @@ export default defineCommand({
|
|
|
310
311
|
candidates = allObs.slice(-20);
|
|
311
312
|
}
|
|
312
313
|
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
byEntity.set(obs.entityName, bucket);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const actions: string[] = [];
|
|
321
|
-
const toResolve: number[] = [];
|
|
322
|
-
for (const [, group] of byEntity) {
|
|
323
|
-
if (group.length < 2) continue;
|
|
324
|
-
for (let index = 0; index < group.length; index += 1) {
|
|
325
|
-
for (let compareIndex = index + 1; compareIndex < group.length; compareIndex += 1) {
|
|
326
|
-
const newer = group[compareIndex];
|
|
327
|
-
const older = group[index];
|
|
328
|
-
try {
|
|
329
|
-
const decision = await deduplicateMemory(
|
|
330
|
-
{ title: newer.title, narrative: newer.narrative, facts: newer.facts },
|
|
331
|
-
[{ id: older.id, title: older.title, narrative: older.narrative, facts: older.facts.join('\n') }],
|
|
332
|
-
);
|
|
333
|
-
if (decision && (decision.action === 'DELETE' || decision.action === 'UPDATE' || decision.action === 'NONE')) {
|
|
334
|
-
actions.push(`Resolve #${older.id} because it duplicates newer #${newer.id}`);
|
|
335
|
-
toResolve.push(older.id);
|
|
336
|
-
}
|
|
337
|
-
} catch {
|
|
338
|
-
// Ignore failed pair analysis so one bad comparison doesn't abort the batch.
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
314
|
+
const plan = await planMemoryDeduplication(candidates);
|
|
315
|
+
const actionLines = plan.actions.map((item) =>
|
|
316
|
+
`Resolve #${item.resolveId} "${item.resolveTitle}"; keep #${item.keepId} "${item.keepTitle}" (${item.reason})`,
|
|
317
|
+
);
|
|
343
318
|
|
|
344
|
-
if (dryRun ||
|
|
319
|
+
if (dryRun || plan.resolveIds.length === 0) {
|
|
345
320
|
emitResult(
|
|
346
|
-
{ project,
|
|
347
|
-
|
|
321
|
+
{ project, plan, resolved: [], dryRun: true },
|
|
322
|
+
actionLines.length === 0 ? 'No duplicate candidates found.' : actionLines.join('\n'),
|
|
348
323
|
asJson,
|
|
349
324
|
);
|
|
350
325
|
return;
|
|
351
326
|
}
|
|
352
327
|
|
|
353
|
-
const result = await
|
|
354
|
-
[...new Set(toResolve)].filter((id) => {
|
|
355
|
-
const observation = getObservation(id, project.id);
|
|
356
|
-
return observation ? canManageObservation(observation, reader) : false;
|
|
357
|
-
}),
|
|
358
|
-
'resolved',
|
|
359
|
-
);
|
|
328
|
+
const result = await applyDeduplicationPlan(getObservationStore(), project.id, plan.resolveIds, reader);
|
|
360
329
|
emitResult(
|
|
361
|
-
{ project,
|
|
330
|
+
{ project, plan, resolved: result.resolved, skipped: result.skipped, dryRun: false },
|
|
362
331
|
`Resolved ${result.resolved.length} duplicate observation(s).`,
|
|
363
332
|
asJson,
|
|
364
333
|
);
|