@jaimevalasek/aioson 1.21.0 → 1.21.3
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 +25 -0
- package/docs/pt/living-memory/reflexao-in-harness.md +2 -0
- package/package.json +1 -1
- package/src/cli.js +5 -0
- package/src/commands/context-health.js +35 -2
- package/src/commands/feature-close.js +36 -0
- package/src/commands/memory-archive.js +193 -193
- package/src/commands/memory-reflect-commit.js +28 -4
- package/src/commands/memory-restore.js +177 -177
- package/src/commands/memory-search.js +135 -135
- package/src/commands/memory-trim.js +191 -0
- package/src/constants.js +1 -0
- package/src/current-state-trim.js +170 -0
- package/src/i18n/messages/en.js +12 -0
- package/src/i18n/messages/es.js +12 -0
- package/src/i18n/messages/fr.js +12 -0
- package/src/i18n/messages/pt-BR.js +12 -0
- package/src/memory-reflect-engine.js +10 -4
- package/template/.aioson/agents/architect.md +3 -0
- package/template/.aioson/agents/committer.md +1 -1
- package/template/.aioson/agents/dev.md +2 -2
- package/template/.aioson/agents/deyvin.md +2 -1
- package/template/.aioson/agents/pentester.md +1 -0
- package/template/.aioson/agents/qa.md +4 -0
- package/template/.aioson/agents/sheldon.md +1 -0
- package/template/.aioson/agents/tester.md +2 -0
- package/template/.aioson/config/autonomy-protocol.json +1 -0
- package/template/.aioson/design-docs/agent-loading-contract.md +138 -0
- package/template/.aioson/docs/quality/code-health-analysis.md +79 -0
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* aioson memory:restore --id=<rule|learning|brain>:<slug> [--reason="<text>"] [--dry-run] [--json]
|
|
5
|
-
*
|
|
6
|
-
* Tier-2 human-actioned command. Moves an archived artifact back to its
|
|
7
|
-
* active path and records an `event_type='restored'` entry with a fresh
|
|
8
|
-
* `start_at` (PMD-10). History is preserved: the prior `archived` entry
|
|
9
|
-
* keeps its `end_at`.
|
|
10
|
-
*
|
|
11
|
-
* Refuses to run when `process.env.AIOSON_RUNTIME_HOOK === '1'` (BR-ALL-01).
|
|
12
|
-
* Emits `aioson notify --level=warn` BEFORE mutation (BR-ALL-06).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const path = require('node:path');
|
|
16
|
-
const { openRuntimeDb } = require('../runtime-store');
|
|
17
|
-
const { runNotify } = require('./notify');
|
|
18
|
-
const {
|
|
19
|
-
parseTargetId,
|
|
20
|
-
normalizeKind,
|
|
21
|
-
restoreTarget,
|
|
22
|
-
TARGET_TYPES
|
|
23
|
-
} = require('../learning-loop-archive');
|
|
24
|
-
|
|
25
|
-
function tFn(t, key, params) {
|
|
26
|
-
if (typeof t === 'function') {
|
|
27
|
-
try { return t(key, params || {}); } catch { /* fall through */ }
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isHookContext() {
|
|
33
|
-
return process.env.AIOSON_RUNTIME_HOOK === '1';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function runMemoryRestore({ args, options = {}, logger, t }) {
|
|
37
|
-
const targetDir = path.resolve(process.cwd(), args && args[0] ? args[0] : '.');
|
|
38
|
-
const wantJson = Boolean(options.json);
|
|
39
|
-
const dryRun = Boolean(options['dry-run'] || options.dryRun);
|
|
40
|
-
const log = (msg) => { if (logger && typeof logger.log === 'function') logger.log(msg); };
|
|
41
|
-
|
|
42
|
-
if (isHookContext()) {
|
|
43
|
-
const msg = tFn(t, 'memory_restore.hook_blocked')
|
|
44
|
-
|| 'memory:restore cannot be invoked from a runtime hook (BR-ALL-01: tier-2 requires human action).';
|
|
45
|
-
if (wantJson) return { ok: false, reason: 'hook_blocked' };
|
|
46
|
-
log(msg);
|
|
47
|
-
return { ok: false, reason: 'hook_blocked' };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const rawId = options.id || options.target || '';
|
|
51
|
-
const reason = options.reason ? String(options.reason).trim() : '';
|
|
52
|
-
|
|
53
|
-
if (!rawId) {
|
|
54
|
-
const msg = tFn(t, 'memory_restore.id_required')
|
|
55
|
-
|| 'memory:restore requires --id=<rule|learning|brain>:<slug>.';
|
|
56
|
-
if (wantJson) return { ok: false, reason: 'missing_id' };
|
|
57
|
-
log(msg);
|
|
58
|
-
return { ok: false, reason: 'missing_id' };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const parsed = parseTargetId(rawId);
|
|
62
|
-
const kind = normalizeKind(parsed.kind);
|
|
63
|
-
if (!kind || !TARGET_TYPES.has(kind) || !parsed.slug) {
|
|
64
|
-
const msg = tFn(t, 'memory_restore.invalid_id', { value: rawId })
|
|
65
|
-
|| `memory:restore invalid --id value: "${rawId}". Expected rule|learning|brain:<slug>.`;
|
|
66
|
-
if (wantJson) return { ok: false, reason: 'invalid_id', value: rawId };
|
|
67
|
-
log(msg);
|
|
68
|
-
return { ok: false, reason: 'invalid_id' };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const notifyMessage = tFn(t, 'memory_restore.notify_template', {
|
|
72
|
-
kind,
|
|
73
|
-
slug: parsed.slug,
|
|
74
|
-
reason: reason || 'restoring archived artifact'
|
|
75
|
-
}) || `restoring ${kind} "${parsed.slug}"${reason ? `: ${reason}` : ''}`;
|
|
76
|
-
let notifyResult;
|
|
77
|
-
try {
|
|
78
|
-
notifyResult = await runNotify({
|
|
79
|
-
args: [targetDir],
|
|
80
|
-
options: {
|
|
81
|
-
level: 'warn',
|
|
82
|
-
topic: 'memory',
|
|
83
|
-
message: notifyMessage,
|
|
84
|
-
agent: 'memory-restore',
|
|
85
|
-
json: wantJson ? true : undefined
|
|
86
|
-
},
|
|
87
|
-
logger: logger || { log: () => {} }
|
|
88
|
-
});
|
|
89
|
-
} catch (err) {
|
|
90
|
-
if (wantJson) return { ok: false, reason: 'notify_failed', error: String(err && err.message || err) };
|
|
91
|
-
log(`memory:restore notify failed: ${err && err.message ? err.message : err}`);
|
|
92
|
-
return { ok: false, reason: 'notify_failed' };
|
|
93
|
-
}
|
|
94
|
-
if (notifyResult && notifyResult.ok === false) {
|
|
95
|
-
if (wantJson) return { ok: false, reason: 'notify_blocked', exitCode: notifyResult.exitCode };
|
|
96
|
-
log('memory:restore aborted: tier-2 notify returned non-zero exit code.');
|
|
97
|
-
return { ok: false, reason: 'notify_blocked' };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
let dbHandle;
|
|
101
|
-
try {
|
|
102
|
-
dbHandle = await openRuntimeDb(targetDir);
|
|
103
|
-
} catch (err) {
|
|
104
|
-
if (wantJson) return { ok: false, reason: 'runtime_db_unavailable', error: String(err && err.message || err) };
|
|
105
|
-
log(`memory:restore runtime db unavailable: ${err && err.message ? err.message : err}`);
|
|
106
|
-
return { ok: false, reason: 'runtime_db_unavailable' };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const { db } = dbHandle;
|
|
110
|
-
let outcome;
|
|
111
|
-
try {
|
|
112
|
-
outcome = restoreTarget(db, {
|
|
113
|
-
targetDir,
|
|
114
|
-
kind,
|
|
115
|
-
slug: parsed.slug,
|
|
116
|
-
reason: reason || null,
|
|
117
|
-
actor: 'human',
|
|
118
|
-
featureSlug: options.feature ? String(options.feature).trim() : null,
|
|
119
|
-
dryRun
|
|
120
|
-
});
|
|
121
|
-
} finally {
|
|
122
|
-
db.close();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (!outcome.ok) {
|
|
126
|
-
if (wantJson) return outcome;
|
|
127
|
-
if (outcome.reason === 'target_not_archived') {
|
|
128
|
-
const msg = tFn(t, 'memory_restore.target_not_archived', { kind, slug: parsed.slug })
|
|
129
|
-
|| `memory:restore: ${kind} "${parsed.slug}" not found in archive.`;
|
|
130
|
-
log(msg);
|
|
131
|
-
} else if (outcome.reason === 'target_already_active') {
|
|
132
|
-
const msg = tFn(t, 'memory_restore.target_already_active', { kind, slug: parsed.slug })
|
|
133
|
-
|| `memory:restore: ${kind} "${parsed.slug}" already active. No-op.`;
|
|
134
|
-
log(msg);
|
|
135
|
-
} else if (outcome.reason === 'target_not_found') {
|
|
136
|
-
const msg = tFn(t, 'memory_restore.target_not_found', { kind, slug: parsed.slug })
|
|
137
|
-
|| `memory:restore: ${kind} "${parsed.slug}" not found.`;
|
|
138
|
-
log(msg);
|
|
139
|
-
} else {
|
|
140
|
-
log(`memory:restore failed: ${outcome.reason}${outcome.error ? ' — ' + outcome.error : ''}`);
|
|
141
|
-
}
|
|
142
|
-
return outcome;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (wantJson) {
|
|
146
|
-
return {
|
|
147
|
-
ok: true,
|
|
148
|
-
dry_run: Boolean(outcome.dryRun),
|
|
149
|
-
kind,
|
|
150
|
-
slug: parsed.slug,
|
|
151
|
-
source_path: outcome.sourcePath,
|
|
152
|
-
dest_path: outcome.destPath,
|
|
153
|
-
restored_entry_id: outcome.restoredEntryId || null,
|
|
154
|
-
start_at: outcome.startAt || null
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (outcome.dryRun) {
|
|
159
|
-
const msg = tFn(t, 'memory_restore.dry_run_summary', {
|
|
160
|
-
kind,
|
|
161
|
-
slug: parsed.slug,
|
|
162
|
-
source: outcome.sourcePath || kind,
|
|
163
|
-
dest: outcome.destPath || '(no dest)'
|
|
164
|
-
}) || `memory:restore [dry-run]: would move ${outcome.sourcePath || kind} → ${outcome.destPath || '(no dest)'}.`;
|
|
165
|
-
log(msg);
|
|
166
|
-
} else {
|
|
167
|
-
const msg = tFn(t, 'memory_restore.restored_success', {
|
|
168
|
-
kind,
|
|
169
|
-
slug: parsed.slug,
|
|
170
|
-
dest: outcome.destPath || ''
|
|
171
|
-
}) || `memory:restore ✓ ${kind} "${parsed.slug}" restored${outcome.destPath ? ' to ' + outcome.destPath : ''}.`;
|
|
172
|
-
log(msg);
|
|
173
|
-
}
|
|
174
|
-
return outcome;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
module.exports = { runMemoryRestore };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson memory:restore --id=<rule|learning|brain>:<slug> [--reason="<text>"] [--dry-run] [--json]
|
|
5
|
+
*
|
|
6
|
+
* Tier-2 human-actioned command. Moves an archived artifact back to its
|
|
7
|
+
* active path and records an `event_type='restored'` entry with a fresh
|
|
8
|
+
* `start_at` (PMD-10). History is preserved: the prior `archived` entry
|
|
9
|
+
* keeps its `end_at`.
|
|
10
|
+
*
|
|
11
|
+
* Refuses to run when `process.env.AIOSON_RUNTIME_HOOK === '1'` (BR-ALL-01).
|
|
12
|
+
* Emits `aioson notify --level=warn` BEFORE mutation (BR-ALL-06).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const { openRuntimeDb } = require('../runtime-store');
|
|
17
|
+
const { runNotify } = require('./notify');
|
|
18
|
+
const {
|
|
19
|
+
parseTargetId,
|
|
20
|
+
normalizeKind,
|
|
21
|
+
restoreTarget,
|
|
22
|
+
TARGET_TYPES
|
|
23
|
+
} = require('../learning-loop-archive');
|
|
24
|
+
|
|
25
|
+
function tFn(t, key, params) {
|
|
26
|
+
if (typeof t === 'function') {
|
|
27
|
+
try { return t(key, params || {}); } catch { /* fall through */ }
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isHookContext() {
|
|
33
|
+
return process.env.AIOSON_RUNTIME_HOOK === '1';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function runMemoryRestore({ args, options = {}, logger, t }) {
|
|
37
|
+
const targetDir = path.resolve(process.cwd(), args && args[0] ? args[0] : '.');
|
|
38
|
+
const wantJson = Boolean(options.json);
|
|
39
|
+
const dryRun = Boolean(options['dry-run'] || options.dryRun);
|
|
40
|
+
const log = (msg) => { if (logger && typeof logger.log === 'function') logger.log(msg); };
|
|
41
|
+
|
|
42
|
+
if (isHookContext()) {
|
|
43
|
+
const msg = tFn(t, 'cli.memory_restore.hook_blocked')
|
|
44
|
+
|| 'memory:restore cannot be invoked from a runtime hook (BR-ALL-01: tier-2 requires human action).';
|
|
45
|
+
if (wantJson) return { ok: false, reason: 'hook_blocked' };
|
|
46
|
+
log(msg);
|
|
47
|
+
return { ok: false, reason: 'hook_blocked' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rawId = options.id || options.target || '';
|
|
51
|
+
const reason = options.reason ? String(options.reason).trim() : '';
|
|
52
|
+
|
|
53
|
+
if (!rawId) {
|
|
54
|
+
const msg = tFn(t, 'cli.memory_restore.id_required')
|
|
55
|
+
|| 'memory:restore requires --id=<rule|learning|brain>:<slug>.';
|
|
56
|
+
if (wantJson) return { ok: false, reason: 'missing_id' };
|
|
57
|
+
log(msg);
|
|
58
|
+
return { ok: false, reason: 'missing_id' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const parsed = parseTargetId(rawId);
|
|
62
|
+
const kind = normalizeKind(parsed.kind);
|
|
63
|
+
if (!kind || !TARGET_TYPES.has(kind) || !parsed.slug) {
|
|
64
|
+
const msg = tFn(t, 'cli.memory_restore.invalid_id', { value: rawId })
|
|
65
|
+
|| `memory:restore invalid --id value: "${rawId}". Expected rule|learning|brain:<slug>.`;
|
|
66
|
+
if (wantJson) return { ok: false, reason: 'invalid_id', value: rawId };
|
|
67
|
+
log(msg);
|
|
68
|
+
return { ok: false, reason: 'invalid_id' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const notifyMessage = tFn(t, 'cli.memory_restore.notify_template', {
|
|
72
|
+
kind,
|
|
73
|
+
slug: parsed.slug,
|
|
74
|
+
reason: reason || 'restoring archived artifact'
|
|
75
|
+
}) || `restoring ${kind} "${parsed.slug}"${reason ? `: ${reason}` : ''}`;
|
|
76
|
+
let notifyResult;
|
|
77
|
+
try {
|
|
78
|
+
notifyResult = await runNotify({
|
|
79
|
+
args: [targetDir],
|
|
80
|
+
options: {
|
|
81
|
+
level: 'warn',
|
|
82
|
+
topic: 'memory',
|
|
83
|
+
message: notifyMessage,
|
|
84
|
+
agent: 'memory-restore',
|
|
85
|
+
json: wantJson ? true : undefined
|
|
86
|
+
},
|
|
87
|
+
logger: logger || { log: () => {} }
|
|
88
|
+
});
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (wantJson) return { ok: false, reason: 'notify_failed', error: String(err && err.message || err) };
|
|
91
|
+
log(`memory:restore notify failed: ${err && err.message ? err.message : err}`);
|
|
92
|
+
return { ok: false, reason: 'notify_failed' };
|
|
93
|
+
}
|
|
94
|
+
if (notifyResult && notifyResult.ok === false) {
|
|
95
|
+
if (wantJson) return { ok: false, reason: 'notify_blocked', exitCode: notifyResult.exitCode };
|
|
96
|
+
log('memory:restore aborted: tier-2 notify returned non-zero exit code.');
|
|
97
|
+
return { ok: false, reason: 'notify_blocked' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let dbHandle;
|
|
101
|
+
try {
|
|
102
|
+
dbHandle = await openRuntimeDb(targetDir);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (wantJson) return { ok: false, reason: 'runtime_db_unavailable', error: String(err && err.message || err) };
|
|
105
|
+
log(`memory:restore runtime db unavailable: ${err && err.message ? err.message : err}`);
|
|
106
|
+
return { ok: false, reason: 'runtime_db_unavailable' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { db } = dbHandle;
|
|
110
|
+
let outcome;
|
|
111
|
+
try {
|
|
112
|
+
outcome = restoreTarget(db, {
|
|
113
|
+
targetDir,
|
|
114
|
+
kind,
|
|
115
|
+
slug: parsed.slug,
|
|
116
|
+
reason: reason || null,
|
|
117
|
+
actor: 'human',
|
|
118
|
+
featureSlug: options.feature ? String(options.feature).trim() : null,
|
|
119
|
+
dryRun
|
|
120
|
+
});
|
|
121
|
+
} finally {
|
|
122
|
+
db.close();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!outcome.ok) {
|
|
126
|
+
if (wantJson) return outcome;
|
|
127
|
+
if (outcome.reason === 'target_not_archived') {
|
|
128
|
+
const msg = tFn(t, 'cli.memory_restore.target_not_archived', { kind, slug: parsed.slug })
|
|
129
|
+
|| `memory:restore: ${kind} "${parsed.slug}" not found in archive.`;
|
|
130
|
+
log(msg);
|
|
131
|
+
} else if (outcome.reason === 'target_already_active') {
|
|
132
|
+
const msg = tFn(t, 'cli.memory_restore.target_already_active', { kind, slug: parsed.slug })
|
|
133
|
+
|| `memory:restore: ${kind} "${parsed.slug}" already active. No-op.`;
|
|
134
|
+
log(msg);
|
|
135
|
+
} else if (outcome.reason === 'target_not_found') {
|
|
136
|
+
const msg = tFn(t, 'cli.memory_restore.target_not_found', { kind, slug: parsed.slug })
|
|
137
|
+
|| `memory:restore: ${kind} "${parsed.slug}" not found.`;
|
|
138
|
+
log(msg);
|
|
139
|
+
} else {
|
|
140
|
+
log(`memory:restore failed: ${outcome.reason}${outcome.error ? ' — ' + outcome.error : ''}`);
|
|
141
|
+
}
|
|
142
|
+
return outcome;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (wantJson) {
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
dry_run: Boolean(outcome.dryRun),
|
|
149
|
+
kind,
|
|
150
|
+
slug: parsed.slug,
|
|
151
|
+
source_path: outcome.sourcePath,
|
|
152
|
+
dest_path: outcome.destPath,
|
|
153
|
+
restored_entry_id: outcome.restoredEntryId || null,
|
|
154
|
+
start_at: outcome.startAt || null
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (outcome.dryRun) {
|
|
159
|
+
const msg = tFn(t, 'cli.memory_restore.dry_run_summary', {
|
|
160
|
+
kind,
|
|
161
|
+
slug: parsed.slug,
|
|
162
|
+
source: outcome.sourcePath || kind,
|
|
163
|
+
dest: outcome.destPath || '(no dest)'
|
|
164
|
+
}) || `memory:restore [dry-run]: would move ${outcome.sourcePath || kind} → ${outcome.destPath || '(no dest)'}.`;
|
|
165
|
+
log(msg);
|
|
166
|
+
} else {
|
|
167
|
+
const msg = tFn(t, 'cli.memory_restore.restored_success', {
|
|
168
|
+
kind,
|
|
169
|
+
slug: parsed.slug,
|
|
170
|
+
dest: outcome.destPath || ''
|
|
171
|
+
}) || `memory:restore ✓ ${kind} "${parsed.slug}" restored${outcome.destPath ? ' to ' + outcome.destPath : ''}.`;
|
|
172
|
+
log(msg);
|
|
173
|
+
}
|
|
174
|
+
return outcome;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { runMemoryRestore };
|
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* aioson memory:search "<query>" — full-text search over project_learnings + promoted rules.
|
|
5
|
-
*
|
|
6
|
-
* Active Learning Loop Phase 2 (DD-4: BM25 default via ORDER BY rank ASC).
|
|
7
|
-
* Tier-1 silent telemetry-wise (no runtime:emit per DD-4 guardrail #8) but writes
|
|
8
|
-
* results to stdout (or JSON if --json) — this is a user-facing CLI verb, not a
|
|
9
|
-
* background hook.
|
|
10
|
-
*
|
|
11
|
-
* Usage:
|
|
12
|
-
* aioson memory:search "<query>" [--limit=5] [--surface=rules|learnings|all]
|
|
13
|
-
* [--include-archived] [--json]
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const path = require('node:path');
|
|
17
|
-
const { openRuntimeDb } = require('../runtime-store');
|
|
18
|
-
const { searchProjectLearnings, QUERY_MAX_CHARS } = require('../learning-loop-fts5');
|
|
19
|
-
|
|
20
|
-
function tFn(t, key, params) {
|
|
21
|
-
if (typeof t === 'function') {
|
|
22
|
-
try { return t(key, params || {}); } catch { /* fall through */ }
|
|
23
|
-
}
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function stripDelimiters(snippet) {
|
|
28
|
-
if (!snippet) return '';
|
|
29
|
-
return String(snippet).replace(/«|»/g, '');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function formatTextResults(results) {
|
|
33
|
-
const lines = [];
|
|
34
|
-
for (let i = 0; i < results.length; i++) {
|
|
35
|
-
const r = results[i];
|
|
36
|
-
lines.push(
|
|
37
|
-
`${i + 1}. [${r.target_type}] ${r.target_id} (status=${r.status}${r.feature_slug ? `, feature=${r.feature_slug}` : ''}, score=${Number(r.score).toFixed(4)})`
|
|
38
|
-
);
|
|
39
|
-
lines.push(` ${stripDelimiters(r.snippet)}`);
|
|
40
|
-
}
|
|
41
|
-
return lines.join('\n');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function runMemorySearch({ args, options = {}, logger, t }) {
|
|
45
|
-
const targetDir = path.resolve(process.cwd(), args[1] !== undefined ? args[1] : '.');
|
|
46
|
-
// Positional 0 is the query string. Some shells will pass it as args[0].
|
|
47
|
-
const query = args[0] !== undefined && args[0] !== null && String(args[0]).trim() !== ''
|
|
48
|
-
? String(args[0])
|
|
49
|
-
: (options.query ? String(options.query) : '');
|
|
50
|
-
|
|
51
|
-
const wantJson = Boolean(options.json);
|
|
52
|
-
const log = (msg) => { if (logger && typeof logger.log === 'function') logger.log(msg); };
|
|
53
|
-
|
|
54
|
-
if (!query || !query.trim()) {
|
|
55
|
-
const msg = tFn(t, 'memory_search.query_empty') || `memory:search requires a non-empty query.`;
|
|
56
|
-
if (wantJson) return { ok: false, reason: 'query_empty' };
|
|
57
|
-
log(msg);
|
|
58
|
-
return { ok: false, reason: 'query_empty' };
|
|
59
|
-
}
|
|
60
|
-
if (query.length > QUERY_MAX_CHARS) {
|
|
61
|
-
const msg = tFn(t, 'memory_search.query_too_long', { max: QUERY_MAX_CHARS })
|
|
62
|
-
|| `memory:search query exceeds ${QUERY_MAX_CHARS} chars.`;
|
|
63
|
-
if (wantJson) return { ok: false, reason: 'query_too_long', max: QUERY_MAX_CHARS };
|
|
64
|
-
log(msg);
|
|
65
|
-
return { ok: false, reason: 'query_too_long' };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const limit = options.limit !== undefined ? Number(options.limit) : 5;
|
|
69
|
-
const surface = options.surface || 'all';
|
|
70
|
-
const includeArchived = Boolean(options['include-archived'] || options.includeArchived);
|
|
71
|
-
|
|
72
|
-
let dbHandle;
|
|
73
|
-
try {
|
|
74
|
-
dbHandle = await openRuntimeDb(targetDir);
|
|
75
|
-
} catch (err) {
|
|
76
|
-
const message = err && err.message ? err.message : String(err);
|
|
77
|
-
if (wantJson) return { ok: false, reason: 'runtime_db_unavailable', error: message };
|
|
78
|
-
log(`memory:search runtime db unavailable: ${message}`);
|
|
79
|
-
return { ok: false, reason: 'runtime_db_unavailable' };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const { db } = dbHandle;
|
|
83
|
-
let outcome;
|
|
84
|
-
try {
|
|
85
|
-
outcome = searchProjectLearnings(db, { query, limit, surface, includeArchived });
|
|
86
|
-
} finally {
|
|
87
|
-
db.close();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!outcome.ok) {
|
|
91
|
-
if (outcome.reason === 'invalid_surface') {
|
|
92
|
-
const msg = tFn(t, 'memory_search.invalid_surface', { value: outcome.value })
|
|
93
|
-
|| `memory:search invalid --surface value: ${outcome.value}.`;
|
|
94
|
-
if (wantJson) return { ok: false, reason: 'invalid_surface', value: outcome.value };
|
|
95
|
-
log(msg);
|
|
96
|
-
return { ok: false, reason: 'invalid_surface' };
|
|
97
|
-
}
|
|
98
|
-
if (outcome.reason === 'query_unparseable') {
|
|
99
|
-
const msg = tFn(t, 'memory_search.query_unparseable', { value: outcome.value })
|
|
100
|
-
|| `memory:search query reduces to empty after sanitization: "${outcome.value}".`;
|
|
101
|
-
if (wantJson) return { ok: false, reason: 'query_unparseable', value: outcome.value };
|
|
102
|
-
log(msg);
|
|
103
|
-
return { ok: false, reason: 'query_unparseable' };
|
|
104
|
-
}
|
|
105
|
-
if (wantJson) return outcome;
|
|
106
|
-
log(`memory:search rejected: ${outcome.reason}`);
|
|
107
|
-
return outcome;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const { results } = outcome;
|
|
111
|
-
if (wantJson) {
|
|
112
|
-
return {
|
|
113
|
-
ok: true,
|
|
114
|
-
query: outcome.query,
|
|
115
|
-
surface: outcome.surface,
|
|
116
|
-
limit: outcome.limit,
|
|
117
|
-
result_count: results.length,
|
|
118
|
-
results
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (results.length === 0) {
|
|
123
|
-
const msg = tFn(t, 'memory_search.no_results', { query }) || `No matches for "${query}".`;
|
|
124
|
-
log(msg);
|
|
125
|
-
return { ok: true, result_count: 0 };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const header = tFn(t, 'memory_search.results_header', { count: results.length, query })
|
|
129
|
-
|| `Top ${results.length} hits for "${query}":`;
|
|
130
|
-
log(header);
|
|
131
|
-
log(formatTextResults(results));
|
|
132
|
-
return { ok: true, result_count: results.length };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
module.exports = { runMemorySearch };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson memory:search "<query>" — full-text search over project_learnings + promoted rules.
|
|
5
|
+
*
|
|
6
|
+
* Active Learning Loop Phase 2 (DD-4: BM25 default via ORDER BY rank ASC).
|
|
7
|
+
* Tier-1 silent telemetry-wise (no runtime:emit per DD-4 guardrail #8) but writes
|
|
8
|
+
* results to stdout (or JSON if --json) — this is a user-facing CLI verb, not a
|
|
9
|
+
* background hook.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* aioson memory:search "<query>" [--limit=5] [--surface=rules|learnings|all]
|
|
13
|
+
* [--include-archived] [--json]
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const { openRuntimeDb } = require('../runtime-store');
|
|
18
|
+
const { searchProjectLearnings, QUERY_MAX_CHARS } = require('../learning-loop-fts5');
|
|
19
|
+
|
|
20
|
+
function tFn(t, key, params) {
|
|
21
|
+
if (typeof t === 'function') {
|
|
22
|
+
try { return t(key, params || {}); } catch { /* fall through */ }
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stripDelimiters(snippet) {
|
|
28
|
+
if (!snippet) return '';
|
|
29
|
+
return String(snippet).replace(/«|»/g, '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatTextResults(results) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
for (let i = 0; i < results.length; i++) {
|
|
35
|
+
const r = results[i];
|
|
36
|
+
lines.push(
|
|
37
|
+
`${i + 1}. [${r.target_type}] ${r.target_id} (status=${r.status}${r.feature_slug ? `, feature=${r.feature_slug}` : ''}, score=${Number(r.score).toFixed(4)})`
|
|
38
|
+
);
|
|
39
|
+
lines.push(` ${stripDelimiters(r.snippet)}`);
|
|
40
|
+
}
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runMemorySearch({ args, options = {}, logger, t }) {
|
|
45
|
+
const targetDir = path.resolve(process.cwd(), args[1] !== undefined ? args[1] : '.');
|
|
46
|
+
// Positional 0 is the query string. Some shells will pass it as args[0].
|
|
47
|
+
const query = args[0] !== undefined && args[0] !== null && String(args[0]).trim() !== ''
|
|
48
|
+
? String(args[0])
|
|
49
|
+
: (options.query ? String(options.query) : '');
|
|
50
|
+
|
|
51
|
+
const wantJson = Boolean(options.json);
|
|
52
|
+
const log = (msg) => { if (logger && typeof logger.log === 'function') logger.log(msg); };
|
|
53
|
+
|
|
54
|
+
if (!query || !query.trim()) {
|
|
55
|
+
const msg = tFn(t, 'cli.memory_search.query_empty') || `memory:search requires a non-empty query.`;
|
|
56
|
+
if (wantJson) return { ok: false, reason: 'query_empty' };
|
|
57
|
+
log(msg);
|
|
58
|
+
return { ok: false, reason: 'query_empty' };
|
|
59
|
+
}
|
|
60
|
+
if (query.length > QUERY_MAX_CHARS) {
|
|
61
|
+
const msg = tFn(t, 'cli.memory_search.query_too_long', { max: QUERY_MAX_CHARS })
|
|
62
|
+
|| `memory:search query exceeds ${QUERY_MAX_CHARS} chars.`;
|
|
63
|
+
if (wantJson) return { ok: false, reason: 'query_too_long', max: QUERY_MAX_CHARS };
|
|
64
|
+
log(msg);
|
|
65
|
+
return { ok: false, reason: 'query_too_long' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const limit = options.limit !== undefined ? Number(options.limit) : 5;
|
|
69
|
+
const surface = options.surface || 'all';
|
|
70
|
+
const includeArchived = Boolean(options['include-archived'] || options.includeArchived);
|
|
71
|
+
|
|
72
|
+
let dbHandle;
|
|
73
|
+
try {
|
|
74
|
+
dbHandle = await openRuntimeDb(targetDir);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const message = err && err.message ? err.message : String(err);
|
|
77
|
+
if (wantJson) return { ok: false, reason: 'runtime_db_unavailable', error: message };
|
|
78
|
+
log(`memory:search runtime db unavailable: ${message}`);
|
|
79
|
+
return { ok: false, reason: 'runtime_db_unavailable' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { db } = dbHandle;
|
|
83
|
+
let outcome;
|
|
84
|
+
try {
|
|
85
|
+
outcome = searchProjectLearnings(db, { query, limit, surface, includeArchived });
|
|
86
|
+
} finally {
|
|
87
|
+
db.close();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!outcome.ok) {
|
|
91
|
+
if (outcome.reason === 'invalid_surface') {
|
|
92
|
+
const msg = tFn(t, 'cli.memory_search.invalid_surface', { value: outcome.value })
|
|
93
|
+
|| `memory:search invalid --surface value: ${outcome.value}.`;
|
|
94
|
+
if (wantJson) return { ok: false, reason: 'invalid_surface', value: outcome.value };
|
|
95
|
+
log(msg);
|
|
96
|
+
return { ok: false, reason: 'invalid_surface' };
|
|
97
|
+
}
|
|
98
|
+
if (outcome.reason === 'query_unparseable') {
|
|
99
|
+
const msg = tFn(t, 'cli.memory_search.query_unparseable', { value: outcome.value })
|
|
100
|
+
|| `memory:search query reduces to empty after sanitization: "${outcome.value}".`;
|
|
101
|
+
if (wantJson) return { ok: false, reason: 'query_unparseable', value: outcome.value };
|
|
102
|
+
log(msg);
|
|
103
|
+
return { ok: false, reason: 'query_unparseable' };
|
|
104
|
+
}
|
|
105
|
+
if (wantJson) return outcome;
|
|
106
|
+
log(`memory:search rejected: ${outcome.reason}`);
|
|
107
|
+
return outcome;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { results } = outcome;
|
|
111
|
+
if (wantJson) {
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
query: outcome.query,
|
|
115
|
+
surface: outcome.surface,
|
|
116
|
+
limit: outcome.limit,
|
|
117
|
+
result_count: results.length,
|
|
118
|
+
results
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (results.length === 0) {
|
|
123
|
+
const msg = tFn(t, 'cli.memory_search.no_results', { query }) || `No matches for "${query}".`;
|
|
124
|
+
log(msg);
|
|
125
|
+
return { ok: true, result_count: 0 };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const header = tFn(t, 'cli.memory_search.results_header', { count: results.length, query })
|
|
129
|
+
|| `Top ${results.length} hits for "${query}":`;
|
|
130
|
+
log(header);
|
|
131
|
+
log(formatTextResults(results));
|
|
132
|
+
return { ok: true, result_count: results.length };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = { runMemorySearch };
|