neurain 0.1.0-alpha.1 → 0.1.0-alpha.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 +22 -0
- package/README.md +19 -3
- package/docs/connect-runtime.en.md +1 -1
- package/docs/connect-runtime.kr.md +1 -1
- package/docs/development-status.en.md +19 -14
- package/docs/development-status.kr.md +19 -14
- package/package.json +5 -2
- package/src/cli.mjs +17 -1
- package/src/core/adopt.mjs +19 -2
- package/src/core/area_index.mjs +32 -5
- package/src/core/capture_durable.mjs +3 -2
- package/src/core/compile_desk.mjs +27 -10
- package/src/core/file_loose.mjs +204 -0
- package/src/core/lint.mjs +4 -0
- package/src/core/onboard.mjs +3 -3
- package/src/core/organize.mjs +297 -0
- package/src/core/reindex.mjs +48 -0
- package/src/core/resolve_target.mjs +92 -0
- package/src/core/structure_audit.mjs +107 -0
- package/src/core/tidy.mjs +167 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- No unreleased changes recorded.
|
|
6
|
+
|
|
7
|
+
## 0.1.0-alpha.3
|
|
8
|
+
|
|
9
|
+
- Destination resolver (`resolve_target.mjs`): `capture` and `compile` now derive one canonical target path from area, write intent, and sensitivity instead of leaving placement to the caller. Private, multi-area, and decision-required items still gate on the `<N>건 저장 진행` confirmation and are never auto-filed.
|
|
10
|
+
|
|
11
|
+
- Janitor (`neurain tidy`): read-only detection of provably-junk residue only (empty Obsidian `Untitled*.canvas/.base`, zero-byte daily notes, stray empty top-level directories); quarantine is recoverable (30-day trash). Registered areas, archives, and structural directories are never touched.
|
|
12
|
+
- Auto-filer (`neurain file`): finds loose durable markdown outside any canonical home and, gated by confirmation, relocates it recoverably and secret-gated; also surfaces read-only `raw/_inbox` routing proposals. Registered area roots and root-contract files are excluded.
|
|
13
|
+
- Structure audit (`neurain structure-audit`): advisory flags for area-adapter gaps, the `output/` vs `outputs/` split, and `sources_map` drift; composed into `lint`. Never writes and never fails a build.
|
|
14
|
+
- New-user organize (`neurain organize`): turns an unknown folder into a searchable area. Reuses the adopt scan, decides a per-file disposition, and on a gated apply scaffolds the area in copy mode (originals are never moved or deleted) and reindexes; hash-keyed receipt with `--rollback`. Secrets and already-structured KBs are protected.
|
|
15
|
+
- Organize and adopt preserve non-ASCII (for example Korean) folder names as area slugs.
|
|
16
|
+
|
|
17
|
+
## 0.1.0-alpha.2
|
|
18
|
+
|
|
19
|
+
- 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).
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## 0.1.0-alpha.1
|
|
23
|
+
|
|
24
|
+
- Capture: file/asset capture + R2/R4/R6 enrichment (overlap candidates, numeric conflicts, sha256 duplicate detection) ported to the engine.
|
|
25
|
+
- 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.
|
|
26
|
+
- 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
27
|
- 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
28
|
- 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
29
|
- 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
|
-
|
|
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-
|
|
162
|
+
- `docs/self-improvement-90-roadmap.en.md`
|
|
163
163
|
- `docs/connect-runtime.en.md`
|
|
164
164
|
|
|
165
165
|
## Existing Folder Adoption
|
|
@@ -176,6 +176,22 @@ Neurain scans first and recommends one of three modes:
|
|
|
176
176
|
|
|
177
177
|
Writes require an explicit confirmation phrase shown by the scan.
|
|
178
178
|
|
|
179
|
+
## Keeping the Vault Tidy
|
|
180
|
+
|
|
181
|
+
Neurain keeps a working vault organized through read-only detection plus gated, recoverable writes. Nothing here moves or deletes a file without an explicit confirmation phrase, and registered areas, archives, and root-contract files are never touched.
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
npx neurain tidy ~/NeurainDemo # report provably-junk residue (read-only)
|
|
185
|
+
npx neurain structure-audit ~/NeurainDemo # advisory structure flags (read-only)
|
|
186
|
+
npx neurain file ~/NeurainDemo # propose homes for loose docs (read-only)
|
|
187
|
+
npx neurain organize ~/SomeUnknownFolder --dry-run # turn an unknown folder into a searchable area
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- `tidy` detects only provably-junk residue (empty Obsidian `Untitled*` canvas/base files, zero-byte daily notes, stray empty top-level directories) and quarantines recoverably.
|
|
191
|
+
- `structure-audit` is advisory only: it flags area-adapter gaps, the `output/` vs `outputs/` split, and `sources_map` drift, and never fails a build.
|
|
192
|
+
- `file` relocates loose durable markdown into its canonical home behind a confirmation gate, and surfaces `raw/_inbox` routing proposals.
|
|
193
|
+
- `organize` turns an unknown folder into a searchable area in copy mode (originals are never moved or deleted), with a hash-keyed receipt and `--rollback`. Secrets and already-structured knowledge bases are protected.
|
|
194
|
+
|
|
179
195
|
## MCP
|
|
180
196
|
|
|
181
197
|
The alpha MCP server is stdio-only:
|
|
@@ -188,7 +204,7 @@ It exposes read/capture/scan/preview tools only. It does not silently compile, p
|
|
|
188
204
|
|
|
189
205
|
## Status
|
|
190
206
|
|
|
191
|
-
This is `0.1.0-alpha.
|
|
207
|
+
This is `0.1.0-alpha.3`. 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
208
|
|
|
193
209
|
Alpha publish command:
|
|
194
210
|
|
|
@@ -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-
|
|
5
|
-
Package: `neurain@0.1.0-alpha.
|
|
6
|
-
Latest documented commit: `
|
|
4
|
+
Last updated: 2026-06-19 KST
|
|
5
|
+
Package: `neurain@0.1.0-alpha.3`
|
|
6
|
+
Latest documented commit: `f3d3849 docs: document auto-organization wave (E27) for alpha.3`
|
|
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
|
|
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
|
-
|
|
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.
|
|
@@ -97,7 +91,7 @@ Status: shipped as publish-ready alpha.
|
|
|
97
91
|
- README, quickstart, safety, privacy, support, pricing, troubleshooting, and release checklist exist.
|
|
98
92
|
- CI workflow, issue templates, PR template, license, and security doc exist.
|
|
99
93
|
- Full readiness verifies npm audit, pack dry-run, and temporary tarball install smoke.
|
|
100
|
-
-
|
|
94
|
+
- Status: npm publish is done. `neurain@0.1.0-alpha.3` is published under the `alpha` tag and the package name is secured.
|
|
101
95
|
|
|
102
96
|
### E26: Thin MCP Connector
|
|
103
97
|
|
|
@@ -110,6 +104,18 @@ Status: shipped.
|
|
|
110
104
|
- MCP server exposes bounded alpha tools for status, search, scan, recall, eval, live-case scaffold, lesson preview, scheduler preview, lifecycle report/eval, and wrap preview.
|
|
111
105
|
- MCP does not expose silent durable wiki writes, lifecycle emit, daemon run/stop, curator write, recall rebuild write, or lesson promotion.
|
|
112
106
|
|
|
107
|
+
### E27: Auto-Organization System
|
|
108
|
+
|
|
109
|
+
Status: shipped.
|
|
110
|
+
|
|
111
|
+
- CLI: `neurain tidy`, `neurain file`, `neurain structure-audit`, `neurain organize`.
|
|
112
|
+
- Destination resolver: `capture` and `compile` derive one canonical target path from area, write intent, and sensitivity. Private, multi-area, and decision-required items still gate on the `<N>건 저장 진행` confirmation and are never auto-filed.
|
|
113
|
+
- Janitor (`tidy`) detects provably-junk residue only (empty Obsidian canvas/base files, zero-byte daily notes, stray empty top-level directories). Read-only; quarantine is recoverable. Registered areas, archives, and structural directories are never touched.
|
|
114
|
+
- Auto-filer (`file`) relocates loose durable markdown into its canonical home behind a confirmation gate, recoverably and secret-gated, and surfaces read-only `raw/_inbox` routing proposals.
|
|
115
|
+
- Structure audit (`structure-audit`) is advisory: it flags area-adapter gaps, the `output/` vs `outputs/` split, and `sources_map` drift, and composes into `lint` without ever writing or failing a build.
|
|
116
|
+
- New-user organize (`organize`) turns an unknown folder into a searchable area in copy mode (originals are never moved or deleted), with a hash-keyed receipt and `--rollback`. Secrets and already-structured KBs are protected. Non-ASCII (for example Korean) folder names are preserved as area slugs.
|
|
117
|
+
- Honest scope: these are vault-hygiene helpers. Detection is read-only and every relocation is gated and recoverable; Neurain does not autonomously reorganize a user's folder.
|
|
118
|
+
|
|
113
119
|
## Current Connector Truth Table
|
|
114
120
|
|
|
115
121
|
| Host | MCP connection | Lifecycle hook preview | Notes |
|
|
@@ -125,7 +131,6 @@ Status: shipped.
|
|
|
125
131
|
- Non-developer GUI.
|
|
126
132
|
- Hosted control plane.
|
|
127
133
|
- Public Trust Center and status page.
|
|
128
|
-
- npm publish or package name reservation.
|
|
129
134
|
- Public GitHub publication if the repo is intentionally kept private during beta.
|
|
130
135
|
|
|
131
136
|
## Documentation Maintenance Rule
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# 개발 진행 상태
|
|
2
2
|
|
|
3
3
|
Version: v0.1
|
|
4
|
-
Last updated: 2026-06-
|
|
5
|
-
Package: `neurain@0.1.0-alpha.
|
|
6
|
-
Latest documented commit: `
|
|
4
|
+
Last updated: 2026-06-19 KST
|
|
5
|
+
Package: `neurain@0.1.0-alpha.3`
|
|
6
|
+
Latest documented commit: `f3d3849 docs: document auto-organization wave (E27) for alpha.3`
|
|
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
|
-
|
|
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.
|
|
@@ -97,7 +91,7 @@ Status: publish-ready alpha로 shipped.
|
|
|
97
91
|
- README, quickstart, safety, privacy, support, pricing, troubleshooting, release checklist가 존재합니다.
|
|
98
92
|
- CI workflow, issue template, PR template, license, security doc이 존재합니다.
|
|
99
93
|
- full readiness가 npm audit, pack dry-run, temporary tarball install smoke를 검증합니다.
|
|
100
|
-
-
|
|
94
|
+
- 상태: npm publish 완료. `neurain@0.1.0-alpha.3`가 `alpha` 태그로 발행됐고 패키지 이름도 선점됨.
|
|
101
95
|
|
|
102
96
|
### E26: Thin MCP Connector
|
|
103
97
|
|
|
@@ -110,6 +104,18 @@ Status: shipped.
|
|
|
110
104
|
- MCP server는 status, search, scan, recall, eval, live-case scaffold, lesson preview, scheduler preview, lifecycle report/eval, wrap preview용 bounded alpha tool을 노출합니다.
|
|
111
105
|
- MCP는 silent durable wiki write, lifecycle emit, daemon run/stop, curator write, recall rebuild write, lesson promotion을 노출하지 않습니다.
|
|
112
106
|
|
|
107
|
+
### E27: 자동 정리 시스템
|
|
108
|
+
|
|
109
|
+
Status: shipped.
|
|
110
|
+
|
|
111
|
+
- CLI: `neurain tidy`, `neurain file`, `neurain structure-audit`, `neurain organize`.
|
|
112
|
+
- 목적지 리졸버: `capture`·`compile`이 영역·쓰기의도·민감도로부터 단일 canonical 목적지 경로를 도출합니다. private·다중영역·결정필요 항목은 여전히 `<N>건 저장 진행` 확인을 거치며 절대 자동 파일링되지 않습니다.
|
|
113
|
+
- Janitor(`tidy`)는 증명 가능한 잔여물만 탐지합니다(빈 Obsidian canvas/base 파일, 0바이트 데일리노트, 빈 최상위 디렉터리). 읽기전용이고 격리는 복구 가능하며, 등록된 영역·아카이브·구조 디렉터리는 건드리지 않습니다.
|
|
114
|
+
- Auto-filer(`file`)는 떠도는 durable 마크다운을 확인 게이트 뒤에서 복구 가능·시크릿 게이트로 canonical 위치로 옮기고, `raw/_inbox` 라우팅 제안을 읽기전용으로 보여줍니다.
|
|
115
|
+
- 구조 점검(`structure-audit`)은 권고용입니다: 영역 어댑터 결손, `output/` 대 `outputs/` 분리, `sources_map` 드리프트를 표시하고 `lint`에 합성되며, 쓰기나 빌드 실패를 일으키지 않습니다.
|
|
116
|
+
- 신규 사용자 organize(`organize`)는 미지의 폴더를 검색 가능한 영역으로 COPY 모드로 전환합니다(원본은 절대 이동·삭제 안 함). 해시 키 receipt + `--rollback`. 시크릿과 이미 구조화된 KB는 보호됩니다. 비ASCII(예: 한글) 폴더명은 영역 슬러그로 보존됩니다.
|
|
117
|
+
- 정직한 범위: 이들은 vault 위생 도구입니다. 탐지는 읽기전용이고 모든 재배치는 게이트·복구 가능하며, Neurain이 사용자 폴더를 자율적으로 재편하지 않습니다.
|
|
118
|
+
|
|
113
119
|
## 현재 connector truth table
|
|
114
120
|
|
|
115
121
|
| Host | MCP connection | Lifecycle hook preview | Notes |
|
|
@@ -125,7 +131,6 @@ Status: shipped.
|
|
|
125
131
|
- non-developer GUI.
|
|
126
132
|
- hosted control plane.
|
|
127
133
|
- public Trust Center 및 status page.
|
|
128
|
-
- npm publish 또는 package name reservation.
|
|
129
134
|
- beta 동안 repo를 private으로 유지한다면 public GitHub publication.
|
|
130
135
|
|
|
131
136
|
## 문서 유지관리 규칙
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neurain",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
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,7 +36,12 @@ 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';
|
|
41
|
+
import { tidyCommand } from './core/tidy.mjs';
|
|
42
|
+
import { structureAuditCommand } from './core/structure_audit.mjs';
|
|
43
|
+
import { fileCommand } from './core/file_loose.mjs';
|
|
44
|
+
import { organizeCommand } from './core/organize.mjs';
|
|
40
45
|
import { sessionPulseCommand } from './core/session_pulse.mjs';
|
|
41
46
|
import { syncCommand } from './core/sync.mjs';
|
|
42
47
|
import { healthCommand } from './core/health.mjs';
|
|
@@ -81,6 +86,7 @@ export async function runCli(argv) {
|
|
|
81
86
|
if (command === 'curator') return render(await curatorCommand(args));
|
|
82
87
|
if (command === 'daemon') return render(await daemonCommand(args));
|
|
83
88
|
if (command === 'recall') return render(await recallCommand(args));
|
|
89
|
+
if (command === 'reindex') return render(await reindexCommand(args));
|
|
84
90
|
if (command === 'status') return render(await statusCommand(args));
|
|
85
91
|
if (command === 'digest') return render(await digestCommand(args));
|
|
86
92
|
if (command === 'queue') return render(await queueCommand(args));
|
|
@@ -103,6 +109,10 @@ export async function runCli(argv) {
|
|
|
103
109
|
if (command === 'hubs') return render(await hubsCommand(args));
|
|
104
110
|
if (command === 'area-index') return render(await areaIndexCommand(args));
|
|
105
111
|
if (command === 'lint') return render(await lintCommand(args));
|
|
112
|
+
if (command === 'tidy') return render(await tidyCommand(args));
|
|
113
|
+
if (command === 'structure-audit') return render(await structureAuditCommand(args));
|
|
114
|
+
if (command === 'file') return render(await fileCommand(args));
|
|
115
|
+
if (command === 'organize') return render(await organizeCommand(args));
|
|
106
116
|
if (command === 'session-pulse') return render(await sessionPulseCommand(args));
|
|
107
117
|
if (command === 'sync') return render(await syncCommand(args));
|
|
108
118
|
if (command === 'health') return render(await healthCommand(args));
|
|
@@ -180,8 +190,11 @@ function helpText() {
|
|
|
180
190
|
Usage:
|
|
181
191
|
neurain init <folder> [--area general] [--lang ko|en] [--dry-run]
|
|
182
192
|
neurain onboard <folder> [--lang ko|en] [--host codex|claude|gemini|runtime] [--json]
|
|
183
|
-
neurain adopt <folder> [--dry-run] [--apply --confirm "<N>건 저장 진행"]
|
|
193
|
+
neurain adopt <folder> [--dry-run] [--apply --confirm "<N>건 저장 진행"] [--no-reindex]
|
|
184
194
|
neurain adopt --rollback <receipt> [--root <folder>]
|
|
195
|
+
neurain organize <folder> [--apply --confirm "<N>건 저장 진행"] [--allow-secret] [--no-reindex] [--json]
|
|
196
|
+
neurain organize --rollback <receipt> --root <folder> [--json]
|
|
197
|
+
neurain reindex <folder> [--dry-run] [--json]
|
|
185
198
|
neurain doctor <folder> [--json]
|
|
186
199
|
neurain search <folder> <query> [--top 10] [--json]
|
|
187
200
|
neurain journal list <folder> [--type type] [--top 20] [--json]
|
|
@@ -237,6 +250,9 @@ Usage:
|
|
|
237
250
|
neurain hubs <folder> [--check | --apply] [--area name] [--json]
|
|
238
251
|
neurain area-index <folder> [--build area | --refresh area | --register-curated area [--sensitivity private] | --detect [--fix] [--restamp-curated]] [--force] [--dry-run] [--json]
|
|
239
252
|
neurain lint <folder> [--json]
|
|
253
|
+
neurain tidy <folder> [--json]
|
|
254
|
+
neurain structure-audit <folder> [--json]
|
|
255
|
+
neurain file <folder> [--apply --confirm "<N>건 저장 진행"] [--allow-secret] [--json]
|
|
240
256
|
neurain session-pulse <folder> --session-id id --summary "..." [--focus t] [--next t] [--max-notes 8] [--no-area-brief] [--queue ...] [--now ts] [--dry-run] [--allow-secret] [--json]
|
|
241
257
|
neurain sync <folder> --session-id id [--summary "..."] [--level light|standard|full] [--no-pulse] [--no-area-brief] [--queue ...] [--now ts] [--dry-run] [--json]
|
|
242
258
|
neurain health <folder> [--fix] [--now ts] [--json]
|
package/src/core/adopt.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { absPath, compactStamp, ensureDir, exists, generatedPath, isTextFile, relPath, safeResolve, sha256,
|
|
4
|
+
import { absPath, compactStamp, ensureDir, exists, generatedPath, isTextFile, relPath, safeResolve, sha256, timestamp, writeFileNoOverwrite } from './fs.mjs';
|
|
5
5
|
import { inferSensitivityFromPath, secretLike } from './safety.mjs';
|
|
6
|
+
import { reindexCommand } from './reindex.mjs';
|
|
7
|
+
import { slugify } from './envelope.mjs';
|
|
6
8
|
|
|
7
9
|
const maxFileBytes = 20 * 1024 * 1024;
|
|
8
10
|
const maxFiles = 5000;
|
|
@@ -17,6 +19,17 @@ export async function adoptCommand(args) {
|
|
|
17
19
|
throw new Error(`Apply requires --confirm "${expected}".`);
|
|
18
20
|
}
|
|
19
21
|
scan.apply = applyAdoption(scan, { root: target });
|
|
22
|
+
// Once the adapters land, register the area and rebuild search in one pass so the
|
|
23
|
+
// adopted folder is immediately searchable. Best-effort: a reindex hiccup must
|
|
24
|
+
// never fail the adoption itself, and --no-reindex opts out.
|
|
25
|
+
if (scan.apply?.ok && !args['no-reindex']) {
|
|
26
|
+
try {
|
|
27
|
+
const r = await reindexCommand({ _: [target], json: true });
|
|
28
|
+
scan.reindex = r.payload || null;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
scan.reindex = { ok: false, error: String((error && error.message) || error) };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
20
33
|
}
|
|
21
34
|
if (args.json) return { json: true, payload: scan };
|
|
22
35
|
return {
|
|
@@ -32,6 +45,7 @@ export async function adoptCommand(args) {
|
|
|
32
45
|
`- Risks: ${scan.summary.risk_count}`,
|
|
33
46
|
`- Confirmation required: ${scan.confirmation_required}`,
|
|
34
47
|
scan.apply ? `- Apply receipt: ${scan.apply.receipt_path}` : '',
|
|
48
|
+
scan.reindex ? `- Reindex: ${scan.reindex.ok ? 'registered + searchable' : 'skipped (' + (scan.reindex.error || 'n/a') + ')'}` : '',
|
|
35
49
|
].filter(Boolean).join('\n'),
|
|
36
50
|
};
|
|
37
51
|
}
|
|
@@ -106,7 +120,10 @@ export function scanAdoption(root, { dryRun = true } = {}) {
|
|
|
106
120
|
: generatedRatio > 0.35
|
|
107
121
|
? 'hybrid'
|
|
108
122
|
: 'in-place';
|
|
109
|
-
|
|
123
|
+
// Hangul-preserving slug so a Korean folder name (e.g. 내폴더) becomes the area
|
|
124
|
+
// name `_내폴더` instead of being stripped to the generic `_adopted-area`. Area
|
|
125
|
+
// ids flow through `[^/]+` matchers and dir-listing discovery, so non-ASCII is safe.
|
|
126
|
+
const areaSlug = slugify(path.basename(root), 'adopted-area');
|
|
110
127
|
const fingerprint = sha256(Buffer.from(files.map((file) => `${file.rel}:${file.sha256}`).sort().join('\n')));
|
|
111
128
|
const required = [
|
|
112
129
|
`10_areas/_${areaSlug}/_area.md`,
|
package/src/core/area_index.mjs
CHANGED
|
@@ -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,
|
|
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: [
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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) {
|
|
@@ -18,6 +18,7 @@ import path from 'node:path';
|
|
|
18
18
|
import { absPath, ensureDir, timestamp } from './fs.mjs';
|
|
19
19
|
import { vaultConfig } from './config.mjs';
|
|
20
20
|
import { firstLineTitle, inferFlushLevelFromEnvelope, inferTargetLayerFromIntent } from './classify.mjs';
|
|
21
|
+
import { resolveCanonicalPath } from './resolve_target.mjs';
|
|
21
22
|
import { folderForSourceType, makeEnvelope, readArgInput, renderCaptureMarkdown, slugify } from './envelope.mjs';
|
|
22
23
|
import { withFileLock, atomicWriteJson } from './durable.mjs';
|
|
23
24
|
import { stageAndPromote } from './stage.mjs';
|
|
@@ -151,7 +152,7 @@ export async function captureCommand(args) {
|
|
|
151
152
|
const { envelope, assetPath, numericConflicts } = buildFullEnvelope(`raw-${timestamp().slice(0, 10).replace(/-/g, '')}-DRYRUN`, 'planned');
|
|
152
153
|
const flushLevel = args['flush-level'] || inferFlushLevelFromEnvelope(envelope);
|
|
153
154
|
const targetLayer = args['target-layer'] || inferTargetLayerFromIntent(envelope.write_intent);
|
|
154
|
-
const queueItem = buildQueueItem(envelope, { sourceId: envelope.source_id, rawPath: envelope.raw_path, assetPath, title, numericConflictCount: numericConflicts.length, session, sessionId, flushLevel, targetLayer, targetPath: args['target-path'] ||
|
|
155
|
+
const queueItem = buildQueueItem(envelope, { sourceId: envelope.source_id, rawPath: envelope.raw_path, assetPath, title, numericConflictCount: numericConflicts.length, session, sessionId, flushLevel, targetLayer, targetPath: args['target-path'] || resolveCanonicalPath(root, vaultCfg, { area_candidates: envelope.area_candidates, write_intent: envelope.write_intent, sensitivity: envelope.sensitivity, title: envelope.title, target_layer: targetLayer, requires_user_decision: envelope.requires_user_decision }).target_path, conflictPolicy: args['conflict-policy'] || 'queue_first', handoffPath: handoffPathFor(vaultCfg, sessionId, session) });
|
|
155
156
|
return done(args, {
|
|
156
157
|
ok: true, command: 'capture', durable_write: false, dry_run: true,
|
|
157
158
|
envelope, queue_item: queueItem, duplicate_of: duplicateOf || undefined,
|
|
@@ -202,7 +203,7 @@ export async function captureCommand(args) {
|
|
|
202
203
|
|
|
203
204
|
const flushLevel = args['flush-level'] || inferFlushLevelFromEnvelope(envelope);
|
|
204
205
|
const targetLayer = args['target-layer'] || inferTargetLayerFromIntent(envelope.write_intent);
|
|
205
|
-
const queueItem = buildQueueItem(envelope, { sourceId, rawPath, assetPath, title, numericConflictCount: numericConflicts.length, session, sessionId, flushLevel, targetLayer, targetPath: args['target-path'] ||
|
|
206
|
+
const queueItem = buildQueueItem(envelope, { sourceId, rawPath, assetPath, title, numericConflictCount: numericConflicts.length, session, sessionId, flushLevel, targetLayer, targetPath: args['target-path'] || resolveCanonicalPath(root, vaultCfg, { area_candidates: envelope.area_candidates, write_intent: envelope.write_intent, sensitivity: envelope.sensitivity, title: envelope.title, target_layer: targetLayer, requires_user_decision: envelope.requires_user_decision }).target_path, conflictPolicy: args['conflict-policy'] || 'queue_first', handoffPath: handoffPathFor(vaultCfg, sessionId, session) });
|
|
206
207
|
fs.appendFileSync(queueAbs, `${JSON.stringify(queueItem)}\n`); // direct: already holding the queue lock
|
|
207
208
|
const pendingCount = pendingCountForSession(root, vaultCfg, sessionId); // after append
|
|
208
209
|
return { refused: false, sourceId, envelope, rawPath, assetPath, pendingCount };
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { absPath, timestamp } from './fs.mjs';
|
|
10
10
|
import { vaultConfig } from './config.mjs';
|
|
11
|
+
import { resolveCanonicalPath } from './resolve_target.mjs';
|
|
11
12
|
import { readJsonSafe, readJsonl } from './vault_state.mjs';
|
|
12
13
|
|
|
13
14
|
function manifestEntries(value) {
|
|
@@ -51,13 +52,29 @@ function reviewReasons(item) {
|
|
|
51
52
|
return [...new Set(reasons)];
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
function summarizeQueueItem(item, reasons = []) {
|
|
55
|
+
function summarizeQueueItem(item, reasons = [], ctx = null) {
|
|
56
|
+
// Fill the canonical destination the pipeline historically left blank: when the
|
|
57
|
+
// queue row carries no explicit target_path, derive it deterministically from
|
|
58
|
+
// area + write_intent + sensitivity so every candidate shows a real save target
|
|
59
|
+
// instead of an empty string (the wiki/area/output drift fix). ctx carries the
|
|
60
|
+
// root + vaultCfg needed to build the path; without it we keep the old behavior.
|
|
61
|
+
let targetPath = item.target_path || '';
|
|
62
|
+
if (!targetPath && ctx && ctx.root && ctx.vaultCfg) {
|
|
63
|
+
targetPath = resolveCanonicalPath(ctx.root, ctx.vaultCfg, {
|
|
64
|
+
area_candidates: item.area_candidates,
|
|
65
|
+
write_intent: item.write_intent,
|
|
66
|
+
sensitivity: item.sensitivity,
|
|
67
|
+
title: item.title,
|
|
68
|
+
target_layer: item.target_layer,
|
|
69
|
+
requires_user_decision: item.requires_user_decision,
|
|
70
|
+
}).target_path;
|
|
71
|
+
}
|
|
55
72
|
return {
|
|
56
73
|
source_id: item.source_id || '',
|
|
57
74
|
title: item.title || '',
|
|
58
75
|
raw_path: item.raw_path || '',
|
|
59
76
|
target_layer: item.target_layer || inferTargetLayer(item),
|
|
60
|
-
target_path:
|
|
77
|
+
target_path: targetPath,
|
|
61
78
|
sensitivity: item.sensitivity || 'internal',
|
|
62
79
|
flush_level: item.flush_level || inferFlushLevel(item),
|
|
63
80
|
reasons,
|
|
@@ -127,13 +144,13 @@ function summarizeManifestTarget(entry, selectedBy) {
|
|
|
127
144
|
};
|
|
128
145
|
}
|
|
129
146
|
|
|
130
|
-
function selectTarget(query, value, queueItems, rawDir) {
|
|
147
|
+
function selectTarget(query, value, queueItems, rawDir, ctx = null) {
|
|
131
148
|
const lower = String(query || '').toLowerCase();
|
|
132
149
|
const sourceIdMatch = String(query || '').match(/\b(?:raw-\d{8}-\d{3}|clipper-\d{8}-\d{6})\b/);
|
|
133
150
|
if (sourceIdMatch) {
|
|
134
151
|
const sourceId = sourceIdMatch[0];
|
|
135
152
|
const queueMatch = queueItems.find((item) => item.source_id === sourceId);
|
|
136
|
-
if (queueMatch) return { kind: 'queue_item', ...summarizeQueueItem(queueMatch, reviewReasons(queueMatch)) };
|
|
153
|
+
if (queueMatch) return { kind: 'queue_item', ...summarizeQueueItem(queueMatch, reviewReasons(queueMatch), ctx) };
|
|
137
154
|
const manifestMatch = manifestEntries(value).find((entry) => entry.source_id === sourceId);
|
|
138
155
|
if (manifestMatch) return summarizeManifestTarget(manifestMatch, 'source_id');
|
|
139
156
|
}
|
|
@@ -156,10 +173,10 @@ function selectTarget(query, value, queueItems, rawDir) {
|
|
|
156
173
|
return null;
|
|
157
174
|
}
|
|
158
175
|
|
|
159
|
-
function selectAutoTarget({ safe: safeItems, needsConfirmation: confirmationItems, manifest: value, sessionId: targetSessionId }) {
|
|
176
|
+
function selectAutoTarget({ safe: safeItems, needsConfirmation: confirmationItems, manifest: value, sessionId: targetSessionId, ctx = null }) {
|
|
160
177
|
if (safeItems.length > 0) {
|
|
161
178
|
const item = safeItems[0];
|
|
162
|
-
return { auto_selected: true, selected_by: 'safe_queue', kind: 'queue_item', ...summarizeQueueItem(item, []) };
|
|
179
|
+
return { auto_selected: true, selected_by: 'safe_queue', kind: 'queue_item', ...summarizeQueueItem(item, [], ctx) };
|
|
163
180
|
}
|
|
164
181
|
if (confirmationItems.length > 0) return null;
|
|
165
182
|
|
|
@@ -217,7 +234,7 @@ export async function compileCommand(args) {
|
|
|
217
234
|
const needsConfirmation = pending
|
|
218
235
|
.filter((item) => reviewReasons(item).length > 0)
|
|
219
236
|
.map((item) => {
|
|
220
|
-
const s = summarizeQueueItem(item, reviewReasons(item));
|
|
237
|
+
const s = summarizeQueueItem(item, reviewReasons(item), { root, vaultCfg });
|
|
221
238
|
// Never expose a private item's file paths in output (R3-#1).
|
|
222
239
|
if (s.sensitivity === 'private') { s.raw_path = '[redacted:private]'; s.target_path = ''; }
|
|
223
240
|
return s;
|
|
@@ -236,10 +253,10 @@ export async function compileCommand(args) {
|
|
|
236
253
|
const wantAuto = Boolean(args.auto);
|
|
237
254
|
const deskOnly = Boolean(args.desk || args['no-auto-select']) || wantList || (!detail && !wantAuto);
|
|
238
255
|
const selected = detail
|
|
239
|
-
? selectTarget(detail, manifest, pending, rawDir)
|
|
256
|
+
? selectTarget(detail, manifest, pending, rawDir, { root, vaultCfg })
|
|
240
257
|
: deskOnly
|
|
241
258
|
? null
|
|
242
|
-
: selectAutoTarget({ safe, needsConfirmation, manifest, sessionId });
|
|
259
|
+
: selectAutoTarget({ safe, needsConfirmation, manifest, sessionId, ctx: { root, vaultCfg } });
|
|
243
260
|
const mode = selected
|
|
244
261
|
? (selected.auto_selected ? 'auto_selected_compile_plan' : 'selected_compile_plan')
|
|
245
262
|
: deskOnly
|
|
@@ -267,7 +284,7 @@ export async function compileCommand(args) {
|
|
|
267
284
|
pending_count: pending.length,
|
|
268
285
|
safe_count: safe.length,
|
|
269
286
|
needs_confirmation_count: needsConfirmation.length,
|
|
270
|
-
safe: safe.slice(0, top).map((item) => summarizeQueueItem(item, [])),
|
|
287
|
+
safe: safe.slice(0, top).map((item) => summarizeQueueItem(item, [], { root, vaultCfg })),
|
|
271
288
|
needs_confirmation: needsConfirmation.slice(0, top),
|
|
272
289
|
},
|
|
273
290
|
source_digest: {
|