neurain 0.1.0-alpha.1 → 0.1.0-alpha.2

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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - No unreleased changes recorded.
6
+
7
+ ## 0.1.0-alpha.2
8
+
9
+ - Documentation currency gate: README and development-status version strings are asserted against package.json in readiness; volatile metric snapshots de-hardcoded (verified green in CI, not pinned in docs).
10
+
11
+
12
+ ## 0.1.0-alpha.1
13
+
14
+ - Capture: file/asset capture + R2/R4/R6 enrichment (overlap candidates, numeric conflicts, sha256 duplicate detection) ported to the engine.
15
+ - Memory-write: completed the generic ledger-primitive set (appendLine/enqueue/setQueueStatus/appendBlock/writeFileGuarded) so a consumer can forward every fact/task primitive to the engine.
16
+ - Release automation: tag-triggered npm publish via Trusted Publishing (OIDC), a push-CI gate (fast readiness + `npm publish --dry-run`), and a prerelease-never-`latest` dist-tag guard.
5
17
  - Expanded the recall markdown corpus to the general area knowledge class (`10_areas/<area>/**`, hubs, area registry), with config-extensible include/exclude (`recall.include`/`recall.exclude`).
6
18
  - Added label-based privacy gating (`labels.mjs`): per-file frontmatter `sensitivity`, area baseline from `_area.md`, and boundary-aware path markers exclude private content at index time. Fixes a substring marker that could exclude a whole legitimate folder.
7
19
  - Added a routed lexical recall branch (`recall_lexical.mjs`): BM25 + structural/layer/entity/domain/fact-ledger boosts, unioned ahead of exact-token and semantic in hybrid search. Auto-enabled when a search index registry has areas or `--area` is set; off by default on a bare vault.
package/README.md CHANGED
@@ -126,7 +126,7 @@ The canonical product development snapshot is maintained in:
126
126
  - `docs/development-status.en.md`
127
127
  - `docs/development-status.kr.md`
128
128
 
129
- As of 2026-06-09 KST, the package is a publish-ready alpha for private beta or alpha user tests. It is not public SaaS GA. The current verified gates are `npm test` 37/37, `node scripts/readiness.mjs --json` score 100, and `npm run readiness -- --json` score 100.
129
+ The package is a publish-ready alpha for private beta or alpha user tests, not public SaaS GA. All gates (`npm test`, the readiness leakage/secret/pack/tarball-install checks, and `npm audit`) run green in CI on every release — see `.github/workflows/release.yml`. The live score is `node scripts/readiness.mjs --json`.
130
130
 
131
131
  Watch reports are also read-only. They do not start a daemon and do not write durable knowledge. They observe recent text files, event journal entries, recap hints, and lesson candidates so a future review worker can decide what deserves attention.
132
132
 
@@ -159,7 +159,7 @@ For multi-area vaults, preview commands do not scan every area by default. Use `
159
159
  See also:
160
160
 
161
161
  - `docs/knowledge-os.en.md`
162
- - `docs/self-improve-90-roadmap.en.md`
162
+ - `docs/self-improvement-90-roadmap.en.md`
163
163
  - `docs/connect-runtime.en.md`
164
164
 
165
165
  ## Existing Folder Adoption
@@ -188,7 +188,7 @@ It exposes read/capture/scan/preview tools only. It does not silently compile, p
188
188
 
189
189
  ## Status
190
190
 
191
- This is `0.1.0-alpha.0`. It is not a public SaaS GA release. The alpha exists to prove installability, local-first onboarding, Codex, Claude, Gemini, and Runtime connectivity, plus safety receipts.
191
+ This is `0.1.0-alpha.2`. It is not a public SaaS GA release. The alpha exists to prove installability, local-first onboarding, Codex, Claude, Gemini, and Runtime connectivity, plus safety receipts.
192
192
 
193
193
  Alpha publish command:
194
194
 
@@ -45,7 +45,7 @@ Safety boundary:
45
45
  - Durable Neurain writes still require explicit CLI confirmation.
46
46
  - The alpha snippet does not support paths containing newline or control bytes.
47
47
  - Scheduler access is read-only and cannot install background jobs or start daemons.
48
- - Scheduler eval access is read-only and checks trigger precision, trigger recall, no-recursion, private-boundary handling, case-file size limits, and target-root non-write snapshots.
48
+ - Scheduler eval access is read-only and checks trigger precision, trigger recall, no-recursion, private-boundary handling, case-file size limits, temp cleanup, and target-root non-write snapshots.
49
49
  - Lifecycle access is read-only through MCP. The runtime can inspect session lineage, while deeper lifecycle event emission uses the separate host-proxy contract below.
50
50
 
51
51
  ## Lifecycle Boundary
@@ -45,7 +45,7 @@ Safety boundary:
45
45
  - Durable Neurain write는 여전히 explicit CLI confirmation이 필요합니다.
46
46
  - Alpha snippet은 newline 또는 control byte가 포함된 path를 지원하지 않습니다.
47
47
  - Scheduler access는 read-only이며 background job을 설치하거나 daemon을 시작할 수 없습니다.
48
- - Scheduler eval access도 read-only이며 trigger precision, trigger recall, no-recursion, private-boundary handling, case-file size limit, target-root non-write snapshot을 확인합니다.
48
+ - Scheduler eval access도 read-only이며 trigger precision, trigger recall, no-recursion, private-boundary handling, case-file size limit, temp cleanup, target-root non-write snapshot을 확인합니다.
49
49
  - Lifecycle access는 MCP에서 read-only입니다. Runtime은 session lineage를 읽을 수 있고, 더 깊은 lifecycle event emission은 아래 host-proxy contract를 사용합니다.
50
50
 
51
51
  ## Lifecycle Boundary
@@ -1,9 +1,9 @@
1
1
  # Development Status
2
2
 
3
3
  Version: v0.1
4
- Last updated: 2026-06-09 KST
5
- Package: `neurain@0.1.0-alpha.0`
6
- Latest documented commit: `3496262 Add E24 onboarding and Gemini MCP connector`
4
+ Last updated: 2026-06-14 KST
5
+ Package: `neurain@0.1.0-alpha.2`
6
+ Latest documented commit: `66016ba docs(dev-status): note unified reindex + adopt auto-reindex in the command surface`
7
7
 
8
8
  This document is the canonical product development snapshot for the public package. It tracks what is shipped, what has evidence, and what must not be claimed yet.
9
9
 
@@ -11,16 +11,9 @@ This document is the canonical product development snapshot for the public packa
11
11
 
12
12
  Neurain is a publish-ready alpha CLI and MCP package. It is ready for private beta or alpha user tests, not public SaaS GA.
13
13
 
14
- Current verified score:
14
+ Current verification: every gate runs green in CI on each release — `npm test`, the full `npm run readiness` suite (leakage/secret scan, `npm audit`, `npm pack --dry-run`, and a temporary tarball-install smoke), and `node scripts/readiness.mjs --json` for the live score. See `.github/workflows/release.yml`.
15
15
 
16
- | Gate | Result |
17
- |---|---:|
18
- | `npm test` | 37/37 pass |
19
- | `node scripts/readiness.mjs --json` | `ok:true`, score `100`, 35 checks |
20
- | `npm run readiness -- --json` | `ok:true`, score `100` |
21
- | `npm audit` | 0 vulnerabilities |
22
- | `npm pack --dry-run` | pass |
23
- | Temporary tarball install smoke | pass |
16
+ Volatile metrics (exact test counts, readiness scores, check counts) are intentionally NOT pinned in this document — they are verified green in CI, not hand-copied, so this snapshot cannot silently drift from the suite.
24
17
 
25
18
  ## Shipped Development
26
19
 
@@ -28,6 +21,7 @@ Current verified score:
28
21
 
29
22
  - Starter vault initialization with `neurain init`.
30
23
  - Existing folder adoption scan, apply, receipt, and rollback.
24
+ - Unified `reindex` (register areas + per-area index refresh + recall rebuild in one pass; curated indexes are never auto-overwritten); `adopt --apply` auto-reindexes so an adopted folder is registered and searchable in one command.
31
25
  - Local doctor, search, capabilities, recap, and wrap preview.
32
26
  - Event journal add/list/verify with explicit confirmation.
33
27
  - Lesson list, candidates, eval, promotion, and rollback.
@@ -1,9 +1,9 @@
1
1
  # 개발 진행 상태
2
2
 
3
3
  Version: v0.1
4
- Last updated: 2026-06-09 KST
5
- Package: `neurain@0.1.0-alpha.0`
6
- Latest documented commit: `3496262 Add E24 onboarding and Gemini MCP connector`
4
+ Last updated: 2026-06-14 KST
5
+ Package: `neurain@0.1.0-alpha.2`
6
+ Latest documented commit: `66016ba docs(dev-status): note unified reindex + adopt auto-reindex in the command surface`
7
7
 
8
8
  이 문서는 public package 기준의 canonical 개발 상태 스냅샷입니다. 무엇이 shipped인지, 어떤 증거가 있는지, 아직 주장하면 안 되는 것이 무엇인지 함께 기록합니다.
9
9
 
@@ -11,16 +11,9 @@ Latest documented commit: `3496262 Add E24 onboarding and Gemini MCP connector`
11
11
 
12
12
  Neurain은 publish-ready alpha CLI 및 MCP package입니다. private beta 또는 alpha user test에는 들어갈 수 있지만 public SaaS GA는 아닙니다.
13
13
 
14
- 현재 검증된 상태:
14
+ 현재 검증: 매 릴리스마다 모든 게이트가 CI에서 green으로 통과합니다 — `npm test`, 전체 `npm run readiness`(leakage/secret 스캔, `npm audit`, `npm pack --dry-run`, 임시 tarball-install smoke), 라이브 점수는 `node scripts/readiness.mjs --json`. `.github/workflows/release.yml` 참조.
15
15
 
16
- | Gate | Result |
17
- |---|---:|
18
- | `npm test` | 37/37 pass |
19
- | `node scripts/readiness.mjs --json` | `ok:true`, score `100`, 35 checks |
20
- | `npm run readiness -- --json` | `ok:true`, score `100` |
21
- | `npm audit` | 0 vulnerabilities |
22
- | `npm pack --dry-run` | pass |
23
- | Temporary tarball install smoke | pass |
16
+ 휘발성 지표(정확한 테스트 수·readiness 점수·check 수)는 의도적으로 이 문서에 박지 않습니다 — CI에서 green으로 검증되며 손으로 복사하지 않으므로, 이 스냅샷이 suite와 조용히 어긋나지 않습니다.
24
17
 
25
18
  ## shipped 개발 범위
26
19
 
@@ -28,6 +21,7 @@ Neurain은 publish-ready alpha CLI 및 MCP package입니다. private beta 또는
28
21
 
29
22
  - `neurain init` starter vault initialization.
30
23
  - 기존 폴더 adoption scan, apply, receipt, rollback.
24
+ - 통합 `reindex`(영역 등록 + per-area 색인 새로고침 + recall rebuild를 한 번에; 큐레이티드 색인은 자동 덮어쓰기 안 함); `adopt --apply`가 자동 reindex하여 채택한 폴더가 한 명령으로 등록·검색 가능.
31
25
  - local doctor, search, capabilities, recap, wrap preview.
32
26
  - 명시적 confirmation이 필요한 event journal add/list/verify.
33
27
  - lesson list, candidates, eval, promotion, rollback.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neurain",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "Local-first Neurain Knowledge OS CLI and MCP connector.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -27,7 +27,10 @@
27
27
  "scripts": {
28
28
  "test": "node --test",
29
29
  "pack:dry-run": "npm pack --dry-run",
30
- "readiness": "node scripts/readiness.mjs --full"
30
+ "readiness": "node scripts/readiness.mjs --full",
31
+ "docs:check": "node scripts/sync-docs.mjs --check",
32
+ "docs:fix": "node scripts/sync-docs.mjs --write",
33
+ "version": "node scripts/sync-docs.mjs --write && git add -u README.md CHANGELOG.md docs/development-status.en.md docs/development-status.kr.md"
31
34
  },
32
35
  "dependencies": {
33
36
  "@modelcontextprotocol/sdk": "^1.17.5"
package/src/cli.mjs CHANGED
@@ -36,6 +36,7 @@ import { labelCommand } from './core/label.mjs';
36
36
  import { orphansCommand } from './core/orphans.mjs';
37
37
  import { hubsCommand } from './core/hubs.mjs';
38
38
  import { areaIndexCommand } from './core/area_index.mjs';
39
+ import { reindexCommand } from './core/reindex.mjs';
39
40
  import { lintCommand } from './core/lint.mjs';
40
41
  import { sessionPulseCommand } from './core/session_pulse.mjs';
41
42
  import { syncCommand } from './core/sync.mjs';
@@ -81,6 +82,7 @@ export async function runCli(argv) {
81
82
  if (command === 'curator') return render(await curatorCommand(args));
82
83
  if (command === 'daemon') return render(await daemonCommand(args));
83
84
  if (command === 'recall') return render(await recallCommand(args));
85
+ if (command === 'reindex') return render(await reindexCommand(args));
84
86
  if (command === 'status') return render(await statusCommand(args));
85
87
  if (command === 'digest') return render(await digestCommand(args));
86
88
  if (command === 'queue') return render(await queueCommand(args));
@@ -180,8 +182,9 @@ function helpText() {
180
182
  Usage:
181
183
  neurain init <folder> [--area general] [--lang ko|en] [--dry-run]
182
184
  neurain onboard <folder> [--lang ko|en] [--host codex|claude|gemini|runtime] [--json]
183
- neurain adopt <folder> [--dry-run] [--apply --confirm "<N>건 저장 진행"]
185
+ neurain adopt <folder> [--dry-run] [--apply --confirm "<N>건 저장 진행"] [--no-reindex]
184
186
  neurain adopt --rollback <receipt> [--root <folder>]
187
+ neurain reindex <folder> [--dry-run] [--json]
185
188
  neurain doctor <folder> [--json]
186
189
  neurain search <folder> <query> [--top 10] [--json]
187
190
  neurain journal list <folder> [--type type] [--top 20] [--json]
@@ -3,6 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { absPath, compactStamp, ensureDir, exists, generatedPath, isTextFile, relPath, safeResolve, sha256, slug, timestamp, writeFileNoOverwrite } from './fs.mjs';
5
5
  import { inferSensitivityFromPath, secretLike } from './safety.mjs';
6
+ import { reindexCommand } from './reindex.mjs';
6
7
 
7
8
  const maxFileBytes = 20 * 1024 * 1024;
8
9
  const maxFiles = 5000;
@@ -17,6 +18,17 @@ export async function adoptCommand(args) {
17
18
  throw new Error(`Apply requires --confirm "${expected}".`);
18
19
  }
19
20
  scan.apply = applyAdoption(scan, { root: target });
21
+ // Once the adapters land, register the area and rebuild search in one pass so the
22
+ // adopted folder is immediately searchable. Best-effort: a reindex hiccup must
23
+ // never fail the adoption itself, and --no-reindex opts out.
24
+ if (scan.apply?.ok && !args['no-reindex']) {
25
+ try {
26
+ const r = await reindexCommand({ _: [target], json: true });
27
+ scan.reindex = r.payload || null;
28
+ } catch (error) {
29
+ scan.reindex = { ok: false, error: String((error && error.message) || error) };
30
+ }
31
+ }
20
32
  }
21
33
  if (args.json) return { json: true, payload: scan };
22
34
  return {
@@ -32,6 +44,7 @@ export async function adoptCommand(args) {
32
44
  `- Risks: ${scan.summary.risk_count}`,
33
45
  `- Confirmation required: ${scan.confirmation_required}`,
34
46
  scan.apply ? `- Apply receipt: ${scan.apply.receipt_path}` : '',
47
+ scan.reindex ? `- Reindex: ${scan.reindex.ok ? 'registered + searchable' : 'skipped (' + (scan.reindex.error || 'n/a') + ')'}` : '',
35
48
  ].filter(Boolean).join('\n'),
36
49
  };
37
50
  }
@@ -130,9 +130,15 @@ function highConfidenceEntities(ctx, area, relFiles) {
130
130
  byCanonical.set(low, cur);
131
131
  }
132
132
 
133
+ // The recall resolver (recall_intel.resolveAreaPath) reads `source_docs` and always
134
+ // resolves them against `${area_root}/`, so emit AREA-relative paths under that key.
135
+ // Evidence outside the area (raw inbox, wiki/entities hubs) can never resolve under
136
+ // area_root and is hard-excluded from the recall corpus anyway, so drop it here.
137
+ const areaPrefix = `${ctx.areasDir}/${area}/`;
138
+ const toAreaRel = (rel) => (rel.startsWith(areaPrefix) ? rel.slice(areaPrefix.length) : null);
133
139
  return [...byCanonical.values()]
134
140
  .filter((e) => e.confidence === 'high' || e.count >= 2)
135
- .map((e) => ({ id: `entity:${area}:${slug(e.canonical)}`, type: e.type, canonical: e.canonical, aliases: [...e.aliases], confidence: e.confidence, evidence_docs: [...e.evidence].slice(0, 5) }));
141
+ .map((e) => ({ id: `entity:${area}:${slug(e.canonical)}`, type: e.type, canonical: e.canonical, aliases: [...e.aliases], confidence: e.confidence, source_docs: [...e.evidence].map(toAreaRel).filter(Boolean).slice(0, 5) }));
136
142
  }
137
143
 
138
144
  function keywordsFromFolder(name) {
@@ -143,7 +149,7 @@ function domainsFromFolders(ctx, area) {
143
149
  if (!fs.existsSync(dir)) return [];
144
150
  return fs.readdirSync(dir, { withFileTypes: true })
145
151
  .filter((d) => d.isDirectory() && !/^(search-index|_trash|_archive|node_modules)/.test(d.name) && !d.name.startsWith('.'))
146
- .map((d) => ({ id: `domain:${area}:${slug(d.name)}`, label: d.name, keywords: keywordsFromFolder(d.name), boost_paths: [`${ctx.areasDir}/${area}/${d.name}`] }))
152
+ .map((d) => ({ id: `domain:${area}:${slug(d.name)}`, label: d.name, keywords: keywordsFromFolder(d.name), boost_paths: [d.name] }))
147
153
  .filter((d) => d.keywords.length);
148
154
  }
149
155
 
@@ -154,11 +160,23 @@ function saveRegistry(ctx, reg) {
154
160
  atomicWriteJson(path.join(ctx.root, ctx.registryPath), reg);
155
161
  }
156
162
 
163
+ function hasCuratedSource(ctx, area) {
164
+ const dir = `${ctx.areasDir}/${area}/search-index`;
165
+ const ents = readJsonSafe(path.join(ctx.root, `${dir}/entities.json`), null);
166
+ const doms = readJsonSafe(path.join(ctx.root, `${dir}/domain-routing.json`), null);
167
+ return Array.isArray(ents) && ents.length > 0 && Array.isArray(doms);
168
+ }
169
+
157
170
  function buildArea(ctx, area, { force = false } = {}) {
158
171
  const reg = loadRegistry(ctx);
159
172
  const existing = reg.areas?.[area];
160
173
  if (existing?.curated) return { ok: false, area, error: 'curated index, refusing to overwrite (use the curated source)' };
161
174
  if (existing && !force) return { ok: false, area, error: 'already registered; pass --force to rebuild a generated index' };
175
+ // An unregistered area may still carry hand-written index files on disk; never
176
+ // clobber them on an unforced build (register them as curated instead).
177
+ if (!existing && !force && hasCuratedSource(ctx, area)) {
178
+ return { ok: false, area, error: 'existing index files present; use --register-curated to keep them, or --force to overwrite' };
179
+ }
162
180
 
163
181
  const relFiles = areaFiles(ctx, area);
164
182
  const entities = highConfidenceEntities(ctx, area, relFiles);
@@ -175,7 +193,9 @@ function buildArea(ctx, area, { force = false } = {}) {
175
193
  });
176
194
  reg.areas = reg.areas || {};
177
195
  const previous = reg.areas[area] || {};
178
- reg.areas[area] = { ...previous, area_root: `${ctx.areasDir}/${area}`, index_dir: 'search-index', entities: 'entities.json', domain_routing: 'domain-routing.json', generated: true, signature: sig, path_map: previous.path_map || [] };
196
+ // generated and curated are mutually exclusive registry states; assert curated:false
197
+ // so a (re)generated index can never keep a stale curated flag from a prior pass.
198
+ reg.areas[area] = { ...previous, area_root: `${ctx.areasDir}/${area}`, index_dir: 'search-index', entities: 'entities.json', domain_routing: 'domain-routing.json', generated: true, curated: false, signature: sig, path_map: previous.path_map || [] };
179
199
  saveRegistry(ctx, reg);
180
200
  }
181
201
  return { ok: true, area, entities: entities.length, domains: domains.length, scanned: relFiles.length, signature: sig.slice(0, 12), dry_run: ctx.dryRun };
@@ -199,7 +219,9 @@ function registerCurated(ctx, area, { sensitivity } = {}) {
199
219
  const reg = loadRegistry(ctx);
200
220
  reg.areas = reg.areas || {};
201
221
  const previous = reg.areas[area] || {};
202
- reg.areas[area] = { ...previous, area_root: `${ctx.areasDir}/${area}`, index_dir: 'search-index', entities: 'entities.json', domain_routing: 'domain-routing.json', curated: true, ...(sensitivity ? { sensitivity } : {}), signature: sig, path_map: previous.path_map || [] };
222
+ // Clear any stale generated flag: register-curated must leave a clean curated-only
223
+ // state (the previous spread would otherwise preserve generated:true and contradict).
224
+ reg.areas[area] = { ...previous, area_root: `${ctx.areasDir}/${area}`, index_dir: 'search-index', entities: 'entities.json', domain_routing: 'domain-routing.json', curated: true, generated: false, ...(sensitivity ? { sensitivity } : {}), signature: sig, path_map: previous.path_map || [] };
203
225
  saveRegistry(ctx, reg);
204
226
  }
205
227
  return { ok: true, area, mode: 'register-curated', entities: entities.length, domains: domains.length, scanned: relFiles.length, signature: sig.slice(0, 12), sensitivity: sensitivity || null, dry_run: ctx.dryRun };
@@ -237,7 +259,12 @@ function autoTidy(ctx, restampCurated) {
237
259
  const { missing, stale_curated, stale_generated } = detect(ctx);
238
260
  const reg = loadRegistry(ctx);
239
261
  const built = []; const refreshed = []; const restamped_curated = [];
240
- for (const area of missing) if (buildArea(ctx, area, {}).ok) built.push(area);
262
+ for (const area of missing) {
263
+ // Preserve hand-written index files (register as curated) rather than overwrite
264
+ // them with a generated starter; only truly empty/absent indexes are built.
265
+ if (hasCuratedSource(ctx, area)) { if (registerCurated(ctx, area, {}).ok) restamped_curated.push(area); }
266
+ else if (buildArea(ctx, area, {}).ok) built.push(area);
267
+ }
241
268
  for (const area of stale_generated) if (refresh(ctx, area).ok) refreshed.push(area);
242
269
  if (restampCurated) {
243
270
  for (const area of stale_curated) {
@@ -0,0 +1,48 @@
1
+ import { absPath } from './fs.mjs';
2
+ import { areaIndexCommand } from './area_index.mjs';
3
+ import { rebuildRecall } from './recall.mjs';
4
+
5
+ // Unified "make the whole vault's search current" orchestration. One command runs the
6
+ // two layers a fresh or just-changed vault needs:
7
+ // 1. per-area indexes: register any unregistered area, refresh stale GENERATED
8
+ // indexes, and restamp curated signatures. Hand-curated entity/domain files are
9
+ // never overwritten -- areas that drifted and need human re-curation are reported,
10
+ // not auto-changed.
11
+ // 2. the FTS5 recall index: rebuilt so all current markdown is searchable.
12
+ // Thin wrapper over existing primitives: idempotent and non-destructive.
13
+ export async function reindexCommand(args) {
14
+ const root = absPath(args._[0] || args.root || process.cwd());
15
+ const dryRun = Boolean(args['dry-run']);
16
+
17
+ const ai = await areaIndexCommand({ _: [root], detect: true, fix: !dryRun, 'restamp-curated': !dryRun, json: true });
18
+ const aix = ai.payload || {};
19
+
20
+ const recall = await rebuildRecall(root, { dryRun });
21
+
22
+ const payload = {
23
+ ok: recall.ok !== false,
24
+ command: 'reindex',
25
+ dry_run: dryRun,
26
+ area_index: {
27
+ built: aix.built || [],
28
+ refreshed: aix.refreshed || [],
29
+ restamped_curated: aix.restamped_curated || [],
30
+ needs_recuration: aix.needs_recuration || [],
31
+ },
32
+ recall: { indexed_count: recall.indexed_count, durable_write: recall.durable_write },
33
+ };
34
+ if (args.json) return { json: true, payload };
35
+ return {
36
+ text: [
37
+ '# Neurain reindex',
38
+ '',
39
+ `- Root: ${root}`,
40
+ `- Dry run: ${dryRun ? 'yes' : 'no'}`,
41
+ `- Areas registered/built: ${payload.area_index.built.length}`,
42
+ `- Generated areas refreshed: ${payload.area_index.refreshed.length}`,
43
+ `- Curated signatures restamped: ${payload.area_index.restamped_curated.length}`,
44
+ `- Needs hand re-curation: ${payload.area_index.needs_recuration.join(', ') || 'none'}`,
45
+ `- Recall docs indexed: ${payload.recall.indexed_count ?? '?'}`,
46
+ ].join('\n'),
47
+ };
48
+ }