claudeos-core 2.4.4 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.de.md +12 -10
  3. package/README.es.md +12 -10
  4. package/README.fr.md +12 -10
  5. package/README.hi.md +12 -10
  6. package/README.ja.md +12 -10
  7. package/README.ko.md +12 -10
  8. package/README.md +12 -10
  9. package/README.ru.md +12 -10
  10. package/README.vi.md +12 -10
  11. package/README.zh-CN.md +12 -10
  12. package/bin/commands/init.js +121 -24
  13. package/bin/commands/lint.js +2 -0
  14. package/bin/commands/memory.js +10 -3
  15. package/content-validator/index.js +82 -13
  16. package/lib/env-parser.js +98 -12
  17. package/lib/memory-scaffold.js +35 -16
  18. package/manifest-generator/index.js +15 -4
  19. package/package.json +92 -92
  20. package/pass-json-validator/index.js +1 -1
  21. package/pass-prompts/templates/angular/pass3.md +2 -1
  22. package/pass-prompts/templates/common/claude-md-scaffold.md +1 -1
  23. package/pass-prompts/templates/common/pass3a-facts.md +11 -9
  24. package/pass-prompts/templates/common/pass4.md +3 -3
  25. package/pass-prompts/templates/java-spring/pass1.md +10 -2
  26. package/pass-prompts/templates/java-spring/pass3.md +5 -4
  27. package/pass-prompts/templates/kotlin-spring/pass3.md +2 -2
  28. package/pass-prompts/templates/node-express/pass3.md +1 -1
  29. package/pass-prompts/templates/node-fastify/pass3.md +1 -0
  30. package/pass-prompts/templates/node-nestjs/pass3.md +1 -0
  31. package/pass-prompts/templates/node-nextjs/pass3.md +1 -1
  32. package/pass-prompts/templates/node-vite/pass3.md +1 -0
  33. package/pass-prompts/templates/python-django/pass3.md +1 -1
  34. package/pass-prompts/templates/python-fastapi/pass3.md +1 -1
  35. package/pass-prompts/templates/python-flask/pass3.md +1 -0
  36. package/pass-prompts/templates/vue-nuxt/pass3.md +1 -0
  37. package/plan-installer/domain-grouper.js +4 -1
  38. package/plan-installer/index.js +26 -7
  39. package/plan-installer/jvm-detect.js +562 -0
  40. package/plan-installer/pass3-context-builder.js +10 -0
  41. package/plan-installer/prompt-generator.js +18 -2
  42. package/plan-installer/scanners/scan-frontend.js +67 -6
  43. package/plan-installer/scanners/scan-java.js +214 -15
  44. package/plan-installer/scanners/scan-kotlin.js +68 -3
  45. package/plan-installer/scanners/scan-node.js +115 -0
  46. package/plan-installer/scanners/scan-python.js +56 -0
  47. package/plan-installer/source-paths.js +61 -0
  48. package/plan-installer/stack-detector.js +726 -51
  49. package/plan-installer/structure-scanner.js +15 -4
package/README.vi.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/claudeos-core.svg?logo=npm&label=npm)](https://www.npmjs.com/package/claudeos-core)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/claudeos-core/claudeos-core/test.yml?branch=master&logo=github&label=CI)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
- [![tests](https://img.shields.io/badge/tests-736%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
+ [![tests](https://img.shields.io/badge/tests-974%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
6
6
  [![node](https://img.shields.io/node/v/claudeos-core.svg?logo=node.js&logoColor=white&label=node)](https://nodejs.org/)
7
7
  [![license](https://img.shields.io/npm/l/claudeos-core.svg?color=blue)](LICENSE)
8
8
  [![downloads](https://img.shields.io/npm/dm/claudeos-core.svg?logo=npm&color=blue&label=downloads)](https://www.npmjs.com/package/claudeos-core)
@@ -25,7 +25,7 @@ Mỗi khi mở phiên mới, Claude Code lại rơi về mặc định của fra
25
25
 
26
26
  **ClaudeOS-Core sinh lại toàn bộ tài liệu này một cách nhất quán, đọc thẳng từ source code thật.** Đầu tiên, một Node.js scanner duyệt dự án để chốt stack, ORM, cấu trúc package và đường dẫn file. Tiếp đó pipeline 4-pass của Claude viết toàn bộ tài liệu: `CLAUDE.md`, `.claude/rules/` auto-load, standards và skills. Mọi pass đều bị khoá trong một allowlist đường dẫn tường minh, LLM không thoát ra được. Cuối cùng, năm validator soi lại kết quả trước khi xuất.
27
27
 
28
- Nhờ vậy, cùng input thì luôn ra cùng output, byte-identical ở cả 10 ngôn ngữ, và không bịa ra đường dẫn không trong code. (Xem chi tiết ở phần [Điểm khác biệt](#điểm-khác-biệt) bên dưới.)
28
+ Nhờ vậy, cùng input thì luôn ra cùng một `CLAUDE.md` với cấu trúc 8 section cố định, được kiểm bởi cùng 25 structural check ở cả 10 ngôn ngữ, và mọi đường dẫn source được trích dẫn đều được đối chiếu với đĩa. (Xem chi tiết ở phần [Điểm khác biệt](#điểm-khác-biệt) bên dưới.)
29
29
 
30
30
  Với các dự án chạy lâu dài, công cụ còn seed thêm một [Memory Layer](#memory-layer-tùy-chọn-cho-dự-án-dài-hạn) riêng.
31
31
 
@@ -115,7 +115,7 @@ Chạy thử trên [`spring-boot-realworld-example-app`](https://github.com/goth
115
115
  </details>
116
116
 
117
117
  <details>
118
- <summary><strong>Phần thực sự đi vào <code>CLAUDE.md</code> của bạn (trích đoạn thật — Section 1 + 2)</strong></summary>
118
+ <summary><strong>Phần thực sự đi vào <code>CLAUDE.md</code> của bạn (trích đoạn thật — Section 1 + 2; heading được hạ xuống <code>####</code> để hiển thị trong README, file thật dùng <code>## N.</code>)</strong></summary>
119
119
 
120
120
  ```markdown
121
121
  # CLAUDE.md — spring-boot-realworld-example-app
@@ -148,7 +148,7 @@ an XML-driven MyBatis persistence layer and JWT-based authentication.
148
148
  | Test Stack | JUnit Jupiter 5, Mockito, AssertJ, rest-assured, spring-mock-mvc |
149
149
  ```
150
150
 
151
- Mọi giá trị trong bảng trên đều do scanner đọc thẳng từ `build.gradle`, `application.properties` cây source trước khi Claude viết file. Toạ độ dependency, tên file `dev.db`, tên migration `V1__create_tables.sql`, ghi chú "no JPA" — tất cả đều sự thật trích xuất, không chỗ nào phỏng đoán.
151
+ Các dòng stack (Java 11, Spring Boot 2.6.3, Gradle, MyBatis, SQLite, port 8080) đến từ scanner deterministic. Những chi tiết nhỏ hơn toạ độ dependency chính xác, tên file `dev.db`, tên migration `V1__create_tables.sql`, ghi chú "no JPA" — do Pass 1 đọc từ `build.gradle`, `application.properties` và cây source, với các sự thật của scanner làm ràng buộc, rồi được validator đối chiếu lại. Không giá trị nào lấy từ default của framework.
152
152
 
153
153
  </details>
154
154
 
@@ -309,7 +309,7 @@ Các category cùng số prefix ở `rules/` và `standard/` cùng chỉ về m
309
309
  | Bạn là... | Vấn đề được giải quyết |
310
310
  |---|---|
311
311
  | **Solo dev** đang khởi động dự án mới với Claude Code | Khỏi phải dạy lại quy ước cho Claude mỗi phiên. `CLAUDE.md` cùng 8 category trong `.claude/rules/` được sinh ra chỉ trong một lần chạy. |
312
- | **Team lead** quản lý standards dùng chung giữa nhiều repo | `.claude/rules/` rất hay drift mỗi khi đổi tên package, đổi ORM hay đổi response wrapper. ClaudeOS-Core đồng bộ lại một cách nhất quán: cùng input thì ra output byte-identical, không diff thừa. |
312
+ | **Team lead** quản lý standards dùng chung giữa nhiều repo | `.claude/rules/` rất hay drift mỗi khi đổi tên package, đổi ORM hay đổi response wrapper. ClaudeOS-Core sinh lại theo một scaffold 8 section cố định: repo nào cũng cùng cấu trúc, cùng verdict của validator, nên diff chỉ cho thấy thay đổi về convention chứ không phải nhiễu layout. |
313
313
  | **Đã quen Claude Code** nhưng chán cảnh phải sửa lại code mỗi lần | Sai response wrapper, sai cấu trúc package, viết JPA trong khi dự án dùng MyBatis, rải `try/catch` trong khi đã có middleware tập trung. Scanner trích quy ước thật của dự án, mỗi pass của Claude đều chạy trong allowlist tường minh. |
314
314
  | **Onboard vào repo mới** (dự án có sẵn, vào team mới) | Chạy `init` xong là có ngay tấm bản đồ kiến trúc sống. Bảng stack trong CLAUDE.md, rules theo từng layer kèm ví dụ ✅/❌, decision log seed sẵn lý do đằng sau những lựa chọn lớn (JPA hay MyBatis, REST hay GraphQL...). Đọc 5 file vẫn nhanh hơn lội qua 5.000 file source. |
315
315
  | **Làm việc bằng tiếng Hàn, Nhật, Trung và 7 ngôn ngữ khác** | Phần lớn rule generator cho Claude Code chỉ có tiếng Anh. ClaudeOS-Core viết toàn bộ ở **10 ngôn ngữ** (`en/ko/ja/zh-CN/es/vi/hi/ru/fr/de`) và áp **kiểm tra cấu trúc byte-identical**. Verdict của `claude-md-validator` không đổi dù chọn ngôn ngữ output nào. |
@@ -331,7 +331,7 @@ Cách này: Code đọc stack của bạn → Code đưa fact đã xác nh
331
331
 
332
332
  Pipeline gồm **ba giai đoạn**, code có mặt ở cả hai phía của lời gọi LLM.
333
333
 
334
- **1. Step A — Scanner (nhất quán, không gọi LLM).** Một Node.js scanner đi qua thư mục gốc, đọc `package.json`, `build.gradle`, `pom.xml`, `pyproject.toml`, parse các file `.env*` (đồng thời redact các biến nhạy cảm như `PASSWORD/SECRET/TOKEN/JWT_SECRET/...`), phân loại pattern kiến trúc (5 pattern A/B/C/D/E của Java; Kotlin CQRS hoặc multi-module; Next.js App Router so với Pages Router; FSD; components-pattern), tìm ra các domain, rồi dựng allowlist tường minh chứa mọi đường dẫn source thật. Output là `project-analysis.json`, đóng vai trò single source of truth cho mọi bước về sau.
334
+ **1. Step A — Scanner (nhất quán, không gọi LLM).** Một Node.js scanner đi qua thư mục gốc, đọc `package.json`, `build.gradle`, `build.gradle.kts`, `pom.xml`, `pyproject.toml`, parse các file `.env*` (đồng thời redact các biến nhạy cảm như `PASSWORD/SECRET/TOKEN/JWT_SECRET/...`), phân loại pattern kiến trúc (5 pattern A/B/C/D/E của Java; Kotlin CQRS hoặc multi-module; Next.js App Router so với Pages Router; FSD; components-pattern), tìm ra các domain, rồi dựng allowlist tường minh chứa mọi đường dẫn source thật. Output là `project-analysis.json`, đóng vai trò single source of truth cho mọi bước về sau.
335
335
 
336
336
  **2. Step B — Pipeline Claude 4-pass (ràng buộc bởi fact của Step A).**
337
337
  - **Pass 1** đọc các file đại diện theo từng domain group rồi trích ra khoảng 50–100 quy ước cho mỗi domain: response wrapper, thư viện logging, cách xử lý lỗi, quy ước naming, pattern test. Mỗi domain group chỉ chạy một lần (`max 4 domains, 40 files per group`) nên context không bao giờ tràn.
@@ -358,12 +358,14 @@ Cốt lõi gắn kết tất cả lại là một bất biến: **Claude chỉ
358
358
 
359
359
  12 stack, tự động phát hiện từ chính các file dự án.
360
360
 
361
- **Backend:** Java/Spring Boot · Kotlin/Spring Boot · Node/Express · Node/Fastify · Node/NestJS · Python/Django · Python/FastAPI · Python/Flask
361
+ **Backend:** Java/Spring Boot (và Spring Framework không dùng Boot, xem bên dưới) · Kotlin/Spring Boot · Node/Express · Node/Fastify · Node/NestJS · Python/Django · Python/FastAPI · Python/Flask
362
362
 
363
363
  **Frontend:** Node/Next.js · Node/Vite · Angular · Vue/Nuxt
364
364
 
365
365
  Dự án multi-stack (chẳng hạn Spring Boot backend cộng Next.js frontend) chạy được luôn mà không cần điều chỉnh.
366
366
 
367
+ **Java cũ là mục tiêu hạng nhất (v2.5.1).** Spring 1.x–6.x không dùng Boot được nhận diện: Gradle (thời `apply plugin:`, cách viết `group:/name:/version:` theo thứ tự bất kỳ, phân giải biến qua `gradle.properties` / `apply from:` / buildSrc, version catalog, root chỉ có `settings.gradle`), Maven (POM thời Maven 2, property `${spring.version}`, `spring-framework-bom`, root multi-module, các project anh em không có POM gốc), **Ant + Ivy**, **metadata của Eclipse / IntelliJ / NetBeans** (`.classpath` kể cả tham chiếu tới JAR không commit, mức compliance trong `.settings`, `.idea/misc.xml`, `nbproject`), `WebContent/WEB-INF/lib/**/*.jar`, `WEB-INF/web.xml`, version schema XSD của Spring và **eGovFrame** — kèm framework, version Spring Framework, mức Java, packaging `war`/`ear`, driver JDBC và ORM. Cây source lấy `src/` làm gốc cũng được quét bằng đúng các domain pattern như `src/main/java`. Mọi version báo ra đều đọc từ file build, tên JAR hoặc property định nghĩa trong chính project; không suy ra từ giá trị mặc định của framework. Project JVM hoàn toàn không có Spring (`java-library`, `application`, `war` chỉ dùng servlet) được báo là Java với `framework: null`, không bao giờ là Spring.
368
+
367
369
  Quy tắc phát hiện và những gì mỗi scanner trích xuất được mô tả trong [docs/vi/stacks.md](docs/vi/stacks.md).
368
370
 
369
371
  ---
@@ -393,7 +395,7 @@ Phần lớn công cụ tài liệu cho Claude Code sinh ra từ một bản mô
393
395
 
394
396
  Cách làm này dẫn tới ba điểm khác biệt cụ thể.
395
397
 
396
- 1. **Stack detection nhất quán.** Cùng dự án, cùng code thì luôn ra cùng output. Hết cảnh "lần này Claude lại đoán khác".
398
+ 1. **Stack detection và cấu trúc nhất quán.** Cùng dự án, cùng code thì luôn ra cùng kết quả scan và cùng layout `CLAUDE.md` 8 section. Câu chữ bên trong từng section vẫn do LLM viết; thứ cố định là các sự thật nó được cung cấp và khuôn nó phải điền vào.
397
399
  2. **Không tạo path bịa.** Prompt của Pass 3 liệt kê tường minh mọi source path được phép, vì thế Claude không thể trích ra path không tồn tại.
398
400
  3. **Hiểu được multi-stack.** Trong cùng một lần chạy, domain backend và frontend dùng prompt phân tích riêng.
399
401
 
@@ -429,7 +431,7 @@ Ngoài pipeline scaffolding kể trên, ClaudeOS-Core còn seed thêm thư mục
429
431
 
430
432
  Bốn file, tất cả đều do Pass 4 viết.
431
433
 
432
- - `decision-log.md` — sổ append-only ghi "vì sao chọn X thay vì Y", seed từ `pass2-merged.json`
434
+ - `decision-log.md` — sổ append-only ghi "vì sao chọn X thay vì Y", seed từ `pass2-merged.json` (không bao giờ bị compact)
433
435
  - `failure-patterns.md` — danh sách lỗi lặp lại kèm điểm frequency và importance
434
436
  - `compaction.md` — cách memory được tự động compact theo thời gian
435
437
  - `auto-rule-update.md` — các pattern nên được nâng thành rule mới
@@ -437,7 +439,7 @@ Bốn file, tất cả đều do Pass 4 viết.
437
439
  Hai lệnh để duy trì layer này lâu dài.
438
440
 
439
441
  ```bash
440
- # Compact log failure-patterns (chạy định kỳ)
442
+ # Compact log failure-patterns (chạy định kỳ; decision-log.md được giữ nguyên)
441
443
  npx claudeos-core memory compact
442
444
 
443
445
  # Đề xuất rule mới từ các failure pattern xuất hiện thường xuyên
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/claudeos-core.svg?logo=npm&label=npm)](https://www.npmjs.com/package/claudeos-core)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/claudeos-core/claudeos-core/test.yml?branch=master&logo=github&label=CI)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
- [![tests](https://img.shields.io/badge/tests-736%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
+ [![tests](https://img.shields.io/badge/tests-974%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
6
6
  [![node](https://img.shields.io/node/v/claudeos-core.svg?logo=node.js&logoColor=white&label=node)](https://nodejs.org/)
7
7
  [![license](https://img.shields.io/npm/l/claudeos-core.svg?color=blue)](LICENSE)
8
8
  [![downloads](https://img.shields.io/npm/dm/claudeos-core.svg?logo=npm&color=blue&label=downloads)](https://www.npmjs.com/package/claudeos-core)
@@ -25,7 +25,7 @@ npx claudeos-core init
25
25
 
26
26
  **ClaudeOS-Core 的做法是直接读源码,稳定地把这些文件重新生成出来。** 第一步,Node.js scanner 扫一遍项目,把技术栈、ORM、包结构、文件路径都摸清楚。第二步,4-pass Claude 流水线产出完整的内容:`CLAUDE.md`、自动加载的 `.claude/rules/`、standards、skills,全部限定在一份明确的路径白名单里,LLM 越不出这个范围。最后,5 个 validator 在交付前再把结果过一遍。
27
27
 
28
- 这样一来,同样的输入永远得到同样的输出。10 种语言里随便选一种,产物在字节层面也完全一致,代码里没有的路径绝不会出现。(细节看下面[有什么不同](#有什么不同)。)
28
+ 这样一来,同样的输入永远得到同样的 8 段式 `CLAUDE.md` 结构。10 种语言里随便选一种,都要通过同样的 25 项结构检查,引用的每一条源码路径都会核对磁盘上是否真实存在。(细节看下面[有什么不同](#有什么不同)。)
29
29
 
30
30
  长期维护的项目,还会顺带生成一份 [Memory Layer](#memory-layer-可选用于长期项目)。
31
31
 
@@ -115,7 +115,7 @@ npx claudeos-core init
115
115
  </details>
116
116
 
117
117
  <details>
118
- <summary><strong>实际写进 <code>CLAUDE.md</code> 的内容 (真实片段 — Section 1 + 2)</strong></summary>
118
+ <summary><strong>实际写进 <code>CLAUDE.md</code> 的内容 (真实片段 — Section 1 + 2;为了 README 渲染把标题降为 <code>####</code>,真实文件用的是 <code>## N.</code>)</strong></summary>
119
119
 
120
120
  ```markdown
121
121
  # CLAUDE.md — spring-boot-realworld-example-app
@@ -148,7 +148,7 @@ an XML-driven MyBatis persistence layer and JWT-based authentication.
148
148
  | Test Stack | JUnit Jupiter 5, Mockito, AssertJ, rest-assured, spring-mock-mvc |
149
149
  ```
150
150
 
151
- 表里的每一项 —— 精确的依赖坐标、`dev.db` 文件名、`V1__create_tables.sql` 迁移名,乃至 "no JPA" 这种判断 —— 都是 Claude 动笔前,scanner `build.gradle`、`application.properties` 和源码目录里实打实读出来的,没有一处靠猜。
151
+ 技术栈那几行 (Java 11、Spring Boot 2.6.3、Gradle、MyBatis、SQLite、端口 8080) 来自确定性的 scanner。更细的信息 —— 精确的依赖坐标、`dev.db` 文件名、`V1__create_tables.sql` 迁移名,乃至 "no JPA" 这种判断 —— Pass 1 以 scanner 确认的事实为约束,从 `build.gradle`、`application.properties` 和源码目录里读出来,再由 validator 交叉核验。没有一处取自框架默认值。
152
152
 
153
153
  </details>
154
154
 
@@ -309,7 +309,7 @@ your-project/
309
309
  | 你是... | 它能帮你解决什么 |
310
310
  |---|---|
311
311
  | **打算用 Claude Code 启动新项目的独立开发者** | 不用每个会话都把规范从头讲一遍。`CLAUDE.md` 加上 8 大类的 `.claude/rules/`,一次跑完就有了。 |
312
- | **要在多个仓库之间维护共享标准的技术负责人** | 重命名包、换 ORM、改响应包装器之后,`.claude/rules/` 总是慢半拍跟不上。ClaudeOS-Core 用一致的方式重新对齐:同样的输入产出字节级一致的结果,diff 里不会有多余噪音。 |
312
+ | **要在多个仓库之间维护共享标准的技术负责人** | 重命名包、换 ORM、改响应包装器之后,`.claude/rules/` 总是慢半拍跟不上。ClaudeOS-Core 按固定的 8 段式 scaffold 重新生成:每个仓库结构相同、validator 判定相同,diff 里看到的是约定的变化,而不是版式噪音。 |
313
313
  | **已经在用 Claude Code,却被生成代码搞得头大的人** | 包装器写错、包结构不对、明明用 MyBatis 却生成 JPA、明明有统一中间件却到处 `try/catch`。Scanner 把项目真实的规范抽出来,每一次 Claude pass 都在明确的路径白名单内运行。 |
314
314
  | **刚加入一个新仓库的人** (老项目、新团队) | 在仓库里跑一下 `init`,就能拿到一张活的架构地图:CLAUDE.md 里的栈表格、按层划分的规则配 ✅/❌ 示例,加上预先写好"为什么"的 decision log (JPA vs MyBatis、REST vs GraphQL 等)。读 5 份文件,胜过翻 5,000 个源文件。 |
315
315
  | **要用中文 / 韩语 / 日语等英语以外的语言工作的人** | 大多数 Claude Code 规则生成器只支持英语。ClaudeOS-Core 把整套产物以 **10 种语言** (`en/ko/ja/zh-CN/es/vi/hi/ru/fr/de`) 输出,而结构校验在所有语言下完全一致 —— `claude-md-validator` 的判定不受输出语言影响。 |
@@ -331,7 +331,7 @@ ClaudeOS-Core 把常见的 Claude Code 流程倒过来跑:
331
331
 
332
332
  整条流水线分**三个阶段**,LLM 调用的两端都有代码把关。
333
333
 
334
- **1. Step A — Scanner (确定性,不调用 LLM)。** Node.js scanner 遍历项目根目录,读取 `package.json` / `build.gradle` / `pom.xml` / `pyproject.toml`,解析 `.env*` 文件 (`PASSWORD/SECRET/TOKEN/JWT_SECRET/...` 这类敏感变量自动脱敏)。接着归类架构模式 (Java 的 5 种 A/B/C/D/E、Kotlin 的 CQRS / 多模块、Next.js 的 App vs Pages Router、FSD、components-pattern),识别业务域,再为每一个真实存在的源文件路径生成一份明确的白名单。结果汇总到 `project-analysis.json`,后续所有步骤都以它为唯一事实来源。
334
+ **1. Step A — Scanner (确定性,不调用 LLM)。** Node.js scanner 遍历项目根目录,读取 `package.json` / `build.gradle` / `build.gradle.kts` / `pom.xml` / `pyproject.toml`,解析 `.env*` 文件 (`PASSWORD/SECRET/TOKEN/JWT_SECRET/...` 这类敏感变量自动脱敏)。接着归类架构模式 (Java 的 5 种 A/B/C/D/E、Kotlin 的 CQRS / 多模块、Next.js 的 App vs Pages Router、FSD、components-pattern),识别业务域,再为每一个真实存在的源文件路径生成一份明确的白名单。结果汇总到 `project-analysis.json`,后续所有步骤都以它为唯一事实来源。
335
335
 
336
336
  **2. Step B — 4-Pass Claude 流水线 (受 Step A 的事实约束)。**
337
337
  - **Pass 1** 按域分组读取代表性文件,从每个域里提炼大约 50–100 条规范:响应包装器、日志库、错误处理、命名约定、测试模式等。每个域分组只跑一次 (`max 4 domains, 40 files per group`),所以 context 不会爆。
@@ -358,12 +358,14 @@ severity 分三档 (`fail` / `warn` / `advisory`),这样用户能手动修掉的
358
358
 
359
359
  12 种技术栈,从项目文件里自动识别:
360
360
 
361
- **Backend:** Java/Spring Boot · Kotlin/Spring Boot · Node/Express · Node/Fastify · Node/NestJS · Python/Django · Python/FastAPI · Python/Flask
361
+ **Backend:** Java/Spring Boot (以及不用 Boot 的 Spring Framework,见下文) · Kotlin/Spring Boot · Node/Express · Node/Fastify · Node/NestJS · Python/Django · Python/FastAPI · Python/Flask
362
362
 
363
363
  **Frontend:** Node/Next.js · Node/Vite · Angular · Vue/Nuxt
364
364
 
365
365
  多栈项目 (例如 Spring Boot 后端 + Next.js 前端) 也能直接跑。
366
366
 
367
+ **遗留 Java 是一等目标 (v2.5.1)。** 可识别不使用 Boot 的 Spring 1.x–6.x:Gradle (`apply plugin:` 时代、顺序任意的 `group:/name:/version:` 写法、通过 `gradle.properties` / `apply from:` / buildSrc 解析变量、版本目录、只有 `settings.gradle` 的根目录)、Maven (Maven 2 时代的 POM、`${spring.version}` 属性、`spring-framework-bom`、多模块根、没有根 POM 的同级项目)、**Ant + Ivy**、**Eclipse / IntelliJ / NetBeans 元数据** (`.classpath`,包括指向未提交 JAR 的引用、`.settings` 中的 compliance 级别、`.idea/misc.xml`、`nbproject`)、`WebContent/WEB-INF/lib/**/*.jar`、`WEB-INF/web.xml`、Spring XSD 架构版本,以及 **eGovFrame** — 连同框架、Spring Framework 版本、Java 级别、`war`/`ear` 打包方式、JDBC 驱动和 ORM 一并给出。以 `src/` 为根的源码树也会用与 `src/main/java` 相同的 domain 模式扫描。报告的每个版本都来自构建文件、JAR 文件名或项目内定义的属性;不会依据框架默认值推测。完全不用 Spring 的 JVM 项目 (`java-library`、`application`、仅有 servlet 的 `war`) 会被报告为 `framework: null` 的 Java,绝不会当成 Spring。
368
+
367
369
  具体的识别规则、每个 scanner 抽取了什么内容,详见 [docs/zh-CN/stacks.md](docs/zh-CN/stacks.md)。
368
370
 
369
371
  ---
@@ -393,7 +395,7 @@ npx claudeos-core health
393
395
 
394
396
  具体落到三件事上:
395
397
 
396
- 1. **栈识别可复现。** 同样的项目 + 同样的代码 = 同样的输出,不会出现"这次 Claude 又写得不一样"的情况。
398
+ 1. **栈识别和结构可复现。** 同样的项目 + 同样的代码 = 同样的扫描结果,同样的 8 段式 `CLAUDE.md` 版式。各段里的措辞仍由 LLM 撰写,固定下来的是交给它的事实和它必须填满的框架。
397
399
  2. **不编造路径。** Pass 3 prompt 里写明了所有允许的源码路径,Claude 没法引用不存在的路径。
398
400
  3. **多栈感知。** 同一次执行里,后端域和前端域走不同的分析 prompt。
399
401
 
@@ -429,7 +431,7 @@ npx claudeos-core health
429
431
 
430
432
  一共 4 个文件,全部由 Pass 4 写入:
431
433
 
432
- - `decision-log.md` —— append-only 的"为什么选 X 不选 Y",从 `pass2-merged.json` 取种子。
434
+ - `decision-log.md` —— append-only 的"为什么选 X 不选 Y",从 `pass2-merged.json` 取种子。(从不压缩)
433
435
  - `failure-patterns.md` —— 反复出现的错误,带 frequency / importance 分数。
434
436
  - `compaction.md` —— 记忆随时间自动压缩的方式。
435
437
  - `auto-rule-update.md` —— 应当晋升为新规则的模式。
@@ -437,7 +439,7 @@ npx claudeos-core health
437
439
  项目持续推进时,这两条命令负责维护这一层:
438
440
 
439
441
  ```bash
440
- # 压缩 failure-patterns 日志 (定期执行)
442
+ # 压缩 failure-patterns 日志 (定期执行;不会动 decision-log.md)
441
443
  npx claudeos-core memory compact
442
444
 
443
445
  # 把高频 failure pattern 提为候选规则
@@ -615,6 +615,42 @@ async function runPass3Split(ctx) {
615
615
  },
616
616
  });
617
617
 
618
+ // v2.5.0 — Deterministic allowlist injection ("LLMs guess, code confirms").
619
+ // The `## Allowed Source Paths` section is the single most important
620
+ // anti-hallucination input for 3b/3c/3d, yet until now it reached
621
+ // pass3a-facts.md only if the LLM faithfully hand-copied up to 500 paths
622
+ // out of pass3-context.json — and the 3a validator only checked file
623
+ // length. Node now writes that section itself from project-analysis.json
624
+ // (replacing any LLM-written version), so the allowlist is byte-exact
625
+ // every run, including resumed runs where 3a was skipped.
626
+ {
627
+ const { injectAllowedPathsSection } = require("../../plan-installer/source-paths");
628
+ let collected = null;
629
+ try {
630
+ const pa = JSON.parse(readFile(path.join(GENERATED_DIR, "project-analysis.json")));
631
+ collected = pa && pa.allowedSourcePaths;
632
+ } catch (_e) { /* tolerated — fallback line is written instead */ }
633
+ const before = readFileSafe(factsFile, "");
634
+ if (before.replace(/^\uFEFF/, "").trim().length === 0) {
635
+ // 3a is marked complete in the marker but the facts file is missing/empty
636
+ // (deleted by hand, or a crash between validate and marker write). Do not
637
+ // fabricate a facts file that contains only the allowlist — 3b would run
638
+ // against an empty fact sheet. Surface it and let the user resume/--force.
639
+ throw new InitError(
640
+ "pass3a-facts.md is missing or empty although Pass 3a is marked complete.\n" +
641
+ " Delete claudeos-core/generated/pass3-complete.json to re-run Pass 3a, or use `init --force`."
642
+ );
643
+ }
644
+ const after = injectAllowedPathsSection(before, collected);
645
+ if (after !== before) {
646
+ if (!writeFileSafe(factsFile, after)) {
647
+ throw new InitError("Failed to write the Allowed Source Paths section into pass3a-facts.md.");
648
+ }
649
+ const n = collected && Array.isArray(collected.paths) ? collected.paths.length : 0;
650
+ log(` 📎 Allowed Source Paths section written by orchestrator (${n} ${collected && collected.mode === "rollup" ? "dirs" : "paths"})`);
651
+ }
652
+ }
653
+
618
654
  // ═══ Stage 3b: CLAUDE.md + standard/ + .claude/rules/ ═══════════
619
655
  //
620
656
  // Single batch (domains ≤ 15): keep legacy "3b" marker (backward-compatible).
@@ -699,8 +735,16 @@ async function runPass3Split(ctx) {
699
735
  }
700
736
  }
701
737
  // For every batch, confirm rules/ was generated (at least one staged-rules move must succeed).
738
+ // Count ONLY claudeos-core-managed categories (`NN.` prefix). Since v2.5.0
739
+ // --force/fresh preserve user-authored entries under .claude/rules/, so a
740
+ // plain recursive count could be >0 even when Pass 3 produced nothing.
702
741
  const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
703
- const rulesCount = countFilesRecursive(rulesDir);
742
+ let rulesCount = 0;
743
+ try {
744
+ for (const e of fs.readdirSync(rulesDir, { withFileTypes: true })) {
745
+ if (e.isDirectory() && /^\d{2}\./.test(e.name)) rulesCount += countFilesRecursive(path.join(rulesDir, e.name));
746
+ }
747
+ } catch (_e) { rulesCount = 0; }
704
748
  if (rulesCount === 0) {
705
749
  problems.push(".claude/rules/ has 0 files (staging-override may have been ignored)");
706
750
  }
@@ -855,7 +899,7 @@ function checkPrerequisites() {
855
899
  );
856
900
  if (!hasProjectMarker) {
857
901
  log(`\n ⚠️ Warning: ${PROJECT_ROOT} does not look like a project root.`);
858
- log(" No .git, package.json, build.gradle, or pom.xml found.");
902
+ log(" No .git, package.json, build.gradle(.kts), pom.xml, or pyproject.toml found.");
859
903
  log(" Run this command from your project directory.\n");
860
904
  }
861
905
 
@@ -910,6 +954,28 @@ async function resolveLanguage(parsedArgs) {
910
954
  return lang;
911
955
  }
912
956
 
957
+ // Remove only the rule categories claudeos-core generates (`NN.` prefixed:
958
+ // 00.core, 10.backend, …, 90.optional). Files/dirs the user authored under
959
+ // .claude/rules/ without that prefix are left untouched. Returns the number
960
+ // of entries removed.
961
+ function wipeManagedRuleCategories(rulesDir) {
962
+ if (!fileExists(rulesDir)) return 0;
963
+ let removed = 0;
964
+ let entries;
965
+ try { entries = fs.readdirSync(rulesDir, { withFileTypes: true }); }
966
+ catch (_e) { return 0; }
967
+ for (const e of entries) {
968
+ // Managed entries are category DIRECTORIES (00.core/, 10.backend/, …).
969
+ // claudeos-core never writes a `NN.`-prefixed FILE at the rules root, so
970
+ // such a file (`.claude/rules/01.team-style.md`) is user-authored — keep it.
971
+ // Guard 2 (zero-rules detection) counts with the same predicate.
972
+ if (!e.isDirectory() || !/^\d{2}\./.test(e.name)) continue;
973
+ fs.rmSync(path.join(rulesDir, e.name), { recursive: true, force: true });
974
+ removed++;
975
+ }
976
+ return removed;
977
+ }
978
+
913
979
  // ─── Stage 3: Resume/Fresh selection ──────────────────────────────
914
980
  // Returns { wasFreshClean: boolean } — the caller uses this to gate the
915
981
  // v1.7.x migration backfill in dispatchPass3.
@@ -930,14 +996,15 @@ async function applyResumeMode(parsedArgs, lang) {
930
996
  // (only .json/.md are unlinked above; directories aren't touched).
931
997
  const stagedDir = path.join(GENERATED_DIR, ".staged-rules");
932
998
  if (fileExists(stagedDir)) fs.rmSync(stagedDir, { recursive: true, force: true });
933
- // Also wipe .claude/rules/ so Guard 2 (zero-rules detection) can't
934
- // false-negative on stale rules from a previous run when the fresh
935
- // Pass 3 run fails silently (e.g. Claude ignores staging-override).
936
- // Step [2] recreates the subdirs from scratch. Any manual edits the
937
- // user made to rule files are lost — acceptable under --force
938
- // ("truly fresh start").
939
- const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
940
- if (fileExists(rulesDir)) fs.rmSync(rulesDir, { recursive: true, force: true });
999
+ // Also wipe the claudeos-core-managed categories under .claude/rules/
1000
+ // so Guard 2 (zero-rules detection) can't false-negative on stale rules
1001
+ // from a previous run when the fresh Pass 3 run fails silently (e.g.
1002
+ // Claude ignores staging-override). Step [2] recreates the subdirs.
1003
+ // Manual edits to GENERATED rule files are lost — acceptable under
1004
+ // --force ("truly fresh start"). Anything the user placed under
1005
+ // .claude/rules/ that is NOT a claudeos-core category (no `NN.` prefix)
1006
+ // is preserved it was never ours to delete.
1007
+ wipeManagedRuleCategories(path.join(PROJECT_ROOT, ".claude/rules"));
941
1008
  wasFreshClean = true;
942
1009
  log(" 🔄 Previous results deleted (--force)\n");
943
1010
  return { wasFreshClean };
@@ -993,11 +1060,9 @@ async function applyResumeMode(parsedArgs, lang) {
993
1060
  // Clean .staged-rules/ leftover from a prior crashed run (same reason as --force branch).
994
1061
  const stagedDir = path.join(GENERATED_DIR, ".staged-rules");
995
1062
  if (fileExists(stagedDir)) fs.rmSync(stagedDir, { recursive: true, force: true });
996
- // Wipe .claude/rules/ for the same Guard 2 false-negative reason as
997
- // the --force branch. Step [2] recreates the subdirs; any manual
998
- // edits are lost — acceptable under an explicit "fresh" choice.
999
- const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
1000
- if (fileExists(rulesDir)) fs.rmSync(rulesDir, { recursive: true, force: true });
1063
+ // Wipe managed rule categories for the same Guard 2 false-negative
1064
+ // reason as the --force branch (user-owned, non-`NN.` entries survive).
1065
+ wipeManagedRuleCategories(path.join(PROJECT_ROOT, ".claude/rules"));
1001
1066
  wasFreshClean = true;
1002
1067
  } else if (mode === "continue" && existingPass1.length === 0 && pass2Exists) {
1003
1068
  // pass2 exists but no pass1 → pass2 is stale, force re-run
@@ -1078,7 +1143,7 @@ function ensureDirectories() {
1078
1143
  }
1079
1144
 
1080
1145
  // ─── Stage 5: Load & validate domain-groups.json ──────────────────
1081
- function loadDomainGroups() {
1146
+ function loadDomainGroups({ wasFreshClean = false } = {}) {
1082
1147
  let domainGroups;
1083
1148
  try {
1084
1149
  domainGroups = JSON.parse(
@@ -1089,6 +1154,34 @@ function loadDomainGroups() {
1089
1154
  }
1090
1155
  const totalGroups = domainGroups.totalGroups;
1091
1156
  if (!totalGroups || typeof totalGroups !== "number" || totalGroups < 1) {
1157
+ if (totalGroups === 0) {
1158
+ let stackLine = "";
1159
+ try {
1160
+ const pa = JSON.parse(readFile(path.join(GENERATED_DIR, "project-analysis.json")));
1161
+ const st = pa.stack || {};
1162
+ stackLine = ` Detected stack: language=${st.language || "none"} framework=${st.framework || "none"} frontend=${st.frontend || "none"}\n`;
1163
+ } catch (_e) { /* best effort */ }
1164
+ throw new InitError(
1165
+ "No domains were found in this project, so there is nothing for Pass 1-3 to analyze.\n" +
1166
+ stackLine +
1167
+ " The scanner recognizes layouts such as:\n" +
1168
+ " Java/Kotlin src/main/java/<pkg>/<domain>/controller/*.java, <pkg>/controller/<domain>/*.java,\n" +
1169
+ " <pkg>/controller/*Controller.java, <pkg>/<domain>/adapter/in/web/*.java,\n" +
1170
+ " <pkg>/<feature>/*Controller.kt, <module>/src/main/java/... (Gradle/Maven multi-module)\n" +
1171
+ " Node src/<domain>/*.ts, src/modules/<domain>/, src/{controllers,routes,services}/<domain>.*\n" +
1172
+ " Python app/<domain>/, app/{routers,routes}/<domain>.py, Django apps with models.py\n" +
1173
+ " Frontend app/<route>/page.tsx (incl. (group)/), pages/<route>/, src/components/<name>/, src/features/<name>/\n" +
1174
+ " If your layout differs, open an issue with your directory tree, or check claudeos-core/generated/project-analysis.json\n" +
1175
+ " to see what was detected. " +
1176
+ (wasFreshClean
1177
+ // --force / "fresh" already wiped the managed rule categories and
1178
+ // generated/*.json|*.md before the scanner ran — say so, do not
1179
+ // claim nothing was touched.
1180
+ ? "CLAUDE.md was not modified, but --force / fresh had already removed the previously generated\n" +
1181
+ " .claude/rules/NN.* categories and claudeos-core/generated/ pass files before this check. Restore them from version control if needed."
1182
+ : "Nothing was written to CLAUDE.md or .claude/rules/.")
1183
+ );
1184
+ }
1092
1185
  throw new InitError(`domain-groups.json has invalid totalGroups: ${totalGroups}\n Re-run plan-installer or check claudeos-core/generated/`);
1093
1186
  }
1094
1187
  if (!domainGroups.groups || totalGroups !== domainGroups.groups.length) {
@@ -1564,7 +1657,8 @@ async function runPass4(opts) {
1564
1657
  // Note: master plan files are no longer generated (previously this
1565
1658
  // included "claudeos-core/plan/50.memory-master.md"). The marker schema
1566
1659
  // still accepts an optional planFiles field for backward compatibility.
1567
- claudeMdAppended: true,
1660
+ // v2.3.0+: CLAUDE.md is never touched by Pass 4 (Pass 3 §8 is authoritative).
1661
+ claudeMdAppended: false,
1568
1662
  }, null, 2);
1569
1663
  return writeFileSafe(pass4Marker, markerBody);
1570
1664
  }
@@ -1625,7 +1719,7 @@ async function runPass4(opts) {
1625
1719
  pass4Label = "Pass 4 already present";
1626
1720
  } else if (!fileExists(pass4PromptFile)) {
1627
1721
  log(" ⚠️ pass4-prompt.md not found — falling back to static scaffold");
1628
- if (applyStaticFallback()) { log(" ✅ Memory/Rules/Plans scaffolded + CLAUDE.md appended (static fallback)"); pass4Label = "Pass 4 (static fallback)"; }
1722
+ if (applyStaticFallback()) { log(" ✅ Memory/Rules/Standard scaffolded (static fallback)"); pass4Label = "Pass 4 (static fallback)"; }
1629
1723
  else { log(" ❌ Static fallback failed to write marker"); pass4Label = "Pass 4 fallback failed"; }
1630
1724
  } else {
1631
1725
  let prompt4 = injectProjectRoot(readFile(pass4PromptFile));
@@ -1675,7 +1769,7 @@ async function runPass4(opts) {
1675
1769
 
1676
1770
  if (!ok4 || !isValidPass4Marker(pass4Marker)) {
1677
1771
  log(" ⚠️ Pass 4 did not produce a valid pass4-memory.json — using static fallback");
1678
- if (applyStaticFallback()) { log(" ✅ Memory/Rules/Plans scaffolded + CLAUDE.md appended (static fallback)"); pass4Label = `Pass 4 (static fallback, ${formatElapsed(elapsed4)})`; }
1772
+ if (applyStaticFallback()) { log(" ✅ Memory/Rules/Standard scaffolded (static fallback)"); pass4Label = `Pass 4 (static fallback, ${formatElapsed(elapsed4)})`; }
1679
1773
  else { log(" ❌ Static fallback failed to write marker"); pass4Label = "Pass 4 fallback failed"; }
1680
1774
  } else {
1681
1775
  // Claude-driven Pass 4 succeeded. Ensure memory + rules + plans + standard + CLAUDE.md append exist
@@ -1744,8 +1838,11 @@ async function runPass4(opts) {
1744
1838
  // ─── Stage 11: Run external verification tools ────────────────────
1745
1839
  function runVerificationTools() {
1746
1840
  const verifyTools = [
1747
- { name: "manifest-generator", script: path.join(TOOLS_DIR, "manifest-generator/index.js") },
1748
- { name: "health-checker", script: path.join(TOOLS_DIR, "health-checker/index.js") },
1841
+ // --sync-skills: only `init` is allowed to let manifest-generator patch
1842
+ // CLAUDE.md §6 / MANIFEST.md (post-generation reconciliation). `health`
1843
+ // runs the same tool without the flag and stays read-only.
1844
+ { name: "manifest-generator", script: path.join(TOOLS_DIR, "manifest-generator/index.js"), args: " --sync-skills" },
1845
+ { name: "health-checker", script: path.join(TOOLS_DIR, "health-checker/index.js"), args: "" },
1749
1846
  ];
1750
1847
 
1751
1848
  for (const t of verifyTools) {
@@ -1753,7 +1850,7 @@ function runVerificationTools() {
1753
1850
  log(` ⏭️ ${t.name} — not found, skipping`);
1754
1851
  continue;
1755
1852
  }
1756
- const ok = run(`node "${t.script}"`, { ignoreError: true });
1853
+ const ok = run(`node "${t.script}"${t.args}`, { ignoreError: true });
1757
1854
  if (!ok) {
1758
1855
  log(` ⚠️ ${t.name} reported issues (non-fatal)`);
1759
1856
  }
@@ -1914,7 +2011,7 @@ async function cmdInit(parsedArgs) {
1914
2011
 
1915
2012
  // ─── [4] Pass 1: Deep analysis per domain group ────────────
1916
2013
  header("[4] Pass 1 — Deep analysis per domain group...");
1917
- const { domainGroups, totalGroups } = loadDomainGroups();
2014
+ const { domainGroups, totalGroups } = loadDomainGroups({ wasFreshClean });
1918
2015
  const pass1Prompts = loadPass1Prompts();
1919
2016
 
1920
2017
  // Progress tracking: Pass 1 (N groups) + Pass 2 + Pass 3 + Pass 4 = totalSteps
@@ -1976,4 +2073,4 @@ async function cmdInit(parsedArgs) {
1976
2073
  printCompletionBanner({ lang, totalGroups, totalStart });
1977
2074
  }
1978
2075
 
1979
- module.exports = { cmdInit, InitError };
2076
+ module.exports = { cmdInit, InitError, wipeManagedRuleCategories };
@@ -32,6 +32,8 @@ Checks:
32
32
  - Section 6 has exactly 3 ### sub-sections
33
33
  - Section 8 has exactly 2 ### sub-sections + 2 #### headings
34
34
  - Each L4 memory file appears in exactly 1 table row (inside Section 8)
35
+ - Each \`## N.\` heading contains its English canonical token (T1)
36
+ - Each section has non-trivial body content (S2, warning)
35
37
 
36
38
  Exit codes:
37
39
  0 — structure valid
@@ -2,7 +2,7 @@
2
2
  * ClaudeOS-Core — Memory command (L4 memory)
3
3
  *
4
4
  * Subcommands:
5
- * memory compact — apply 4-stage compaction to decision-log.md / failure-patterns.md
5
+ * memory compact — apply 4-stage compaction to failure-patterns.md (decision-log.md is append-only)
6
6
  * memory score — recompute importance of failure-patterns.md entries
7
7
  * memory propose-rules — analyze failure patterns, append suggestions to auto-rule-update.md
8
8
  */
@@ -247,7 +247,13 @@ function cmdCompact() {
247
247
  ensureDir(MEMORY_DIR);
248
248
  const activeRulePaths = loadActiveRulePaths();
249
249
 
250
- const files = ["decision-log.md", "failure-patterns.md"];
250
+ // decision-log.md is EXCLUDED from compaction. Its contract (rules/60.memory/
251
+ // 01.decision-log.md, CLAUDE.md §8) is "permanent, append-only". Stage 1
252
+ // summarization dropped the Context/Options/Decision/Consequences body of
253
+ // every entry older than 30 days — i.e. exactly the "why" the file exists
254
+ // to preserve. Only failure-patterns.md carries the frequency/importance
255
+ // metadata the 4-stage policy is designed around.
256
+ const files = ["failure-patterns.md"];
251
257
  const summaries = [];
252
258
  for (const f of files) {
253
259
  const r = compactFile(path.join(MEMORY_DIR, f), activeRulePaths);
@@ -255,6 +261,7 @@ function cmdCompact() {
255
261
  if (r.changed) log(` ✅ ${f}: ${r.before} → ${r.after} entries`);
256
262
  else log(` ⏭️ ${f}: ${r.reason}`);
257
263
  }
264
+ log(" ⏭️ decision-log.md: append-only, never compacted");
258
265
 
259
266
  // Update compaction.md "Last Compaction" section.
260
267
  // Replace ONLY the "## Last Compaction" section (up to next `##` heading or EOF).
@@ -411,7 +418,7 @@ function showHelp() {
411
418
  Usage: npx claudeos-core memory <subcommand>
412
419
 
413
420
  Subcommands:
414
- compact Apply 4-stage compaction to decision-log.md and failure-patterns.md
421
+ compact Apply 4-stage compaction to failure-patterns.md (decision-log.md is append-only, never compacted)
415
422
  score Recompute importance of failure-patterns.md entries (frequency × recency)
416
423
  propose-rules Analyze failure patterns, append rule update suggestions to auto-rule-update.md
417
424
  `);
@@ -423,10 +423,35 @@ async function main() {
423
423
  // check covers both. No natural-language matching involved.
424
424
  console.log(" [10/10] path-claim verification (hallucination + MANIFEST drift)...");
425
425
 
426
- // Regex: matches `src/...` paths to TS/TSX/JS/JSX files, not inside
427
- // inline code already fenced. We still strip fenced blocks first so
428
- // example blocks inside ```...``` don't produce false positives.
429
- const SRC_PATH_RE = /\bsrc\/[\w\-./]+\.(?:ts|tsx|js|jsx)\b/g;
426
+ // Regex: matches `src/...` paths to source files, not inside inline code
427
+ // already fenced. We still strip fenced blocks first so example blocks
428
+ // inside ```...``` don't produce false positives.
429
+ //
430
+ // v2.5.0 — extension list widened. Pre-v2.5.0 only ts/tsx/js/jsx were
431
+ // checked, so Java/Kotlin/Python projects (and MyBatis mapper XML, the
432
+ // single most-hallucinated path class on Spring projects) got zero
433
+ // path-claim coverage despite "no invented paths" being the headline
434
+ // guarantee. Config extensions (yml/properties/json) are deliberately
435
+ // NOT included: those are cited as illustrative profile names
436
+ // (`application-{profile}.yml`) far more often than as path claims.
437
+ //
438
+ // An optional module prefix (`api/src/…`, `apps/web/src/…`,
439
+ // `servers/query/x/src/…`) is captured as part of the claim. A prefixed
440
+ // claim is checked at that exact location only — citing `core/src/…` for a
441
+ // file that lives under `api/` is a wrong-module claim and must be flagged,
442
+ // which the bare-`src/` module search below would otherwise hide.
443
+ //
444
+ // Two alternatives, each with its own left boundary:
445
+ // (1) `<module>/…/src/…` — the prefix may not start right after a word
446
+ // char, `.`, `/` or `@`, so `@acme/ui/src/Button.tsx` (a package
447
+ // import, not a repo path) is NOT captured as module `acme/ui`;
448
+ // (2) bare `src/…` — may be preceded by `/` (so the `src/Button.tsx`
449
+ // tail of that import is still checked via the module search) but
450
+ // not by a word char, so `libsrc/x.ts` never yields `src/x.ts`.
451
+ // Dependency paths (`node_modules/<pkg>/src/…`) are skipped below.
452
+ // `:` is excluded before a prefix so a dev-server URL (`localhost:5173/src/main.tsx`)
453
+ // does not yield the module `5173`; its `src/main.tsx` tail is still checked.
454
+ const SRC_PATH_RE = /(?:(?<![\w\-./@:])[\w\-][\w\-.]*\/(?:[\w\-][\w\-.]*\/)*src\/|(?<![\w\-.@])src\/)[\w\-./]+\.(?:ts|tsx|js|jsx|mjs|cjs|vue|svelte|java|kt|kts|py|xml|sql)\b/g;
430
455
 
431
456
  // Placeholder paths are scaffold templates / teaching examples, not
432
457
  // real path claims. We skip them. Three patterns qualify:
@@ -541,18 +566,59 @@ async function main() {
541
566
  // The monorepo fallback only fires for paths starting with `src/`,
542
567
  // which is the conventional workspace-relative form. Non-`src/` paths
543
568
  // (e.g., `claudeos-core/skills/...`) are checked direct-only.
569
+ // Directories that are never application modules even when they contain a
570
+ // `src/` child: virtualenvs (pip editable installs default to `<venv>/src`),
571
+ // vendored code, docs, tooling, test fixtures.
572
+ //
573
+ // Two tiers: HARD names are never modules at any depth (build output,
574
+ // dependencies, virtualenvs). SOFT names (`docs`, `tools`, `scripts`,
575
+ // `test(s)`, `fixtures`) are skipped only at the project root — inside a
576
+ // workspace container (`apps/docs`, `packages/tools`) they are legitimate
577
+ // packages, and skipping them there produced false STALE_PATH advisories on
578
+ // the default Turborepo layout.
579
+ const MODULE_SCAN_SKIP = new Set(["node_modules", ".git", ".claude", "claudeos-core", "build", "target", "dist", "out", ".gradle", ".idea", "coverage", ".next",
580
+ "venv", ".venv", "env", "virtualenv", "site-packages", "__pycache__", "vendor", "tmp", "temp"]);
581
+ const MODULE_SCAN_SKIP_ROOT_ONLY = new Set(["docs", "doc", "tools", "scripts", "test", "tests", "__tests__", "fixtures"]);
582
+ const WORKSPACE_CONTAINERS = new Set(["apps", "packages", "libs", "services", "servers", "modules"]);
583
+ function listModuleDirs(dir, { root = false, parentName = "" } = {}) {
584
+ let entries;
585
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
586
+ catch (_e) { return []; }
587
+ const inWorkspace = WORKSPACE_CONTAINERS.has(parentName);
588
+ return entries
589
+ .filter(e => e.isDirectory() && !MODULE_SCAN_SKIP.has(e.name) && !e.name.startsWith(".")
590
+ && !((root || !inWorkspace) && MODULE_SCAN_SKIP_ROOT_ONLY.has(e.name)))
591
+ .map(e => path.join(dir, e.name));
592
+ }
593
+ // Memoized once per run: every directory up to three levels deep that
594
+ // contains a `src/` folder (JS workspaces `apps/x`, Gradle modules `api`,
595
+ // and the nested Kotlin CQRS layout `servers/query/x` the scanner itself
596
+ // supports). Computed lazily on the first `src/...` miss so projects with
597
+ // zero misses pay nothing.
598
+ let moduleSrcRoots = null;
599
+ function getModuleSrcRoots(ROOT) {
600
+ if (moduleSrcRoots) return moduleSrcRoots;
601
+ moduleSrcRoots = [];
602
+ const walk = (dir, depth) => {
603
+ for (const child of listModuleDirs(dir, { root: depth === 1, parentName: path.basename(dir) })) {
604
+ if (fs.existsSync(path.join(child, "src"))) moduleSrcRoots.push(child);
605
+ if (depth < 3) walk(child, depth + 1);
606
+ }
607
+ };
608
+ walk(ROOT, 1);
609
+ return moduleSrcRoots;
610
+ }
544
611
  function resolvePathClaim(ROOT, claimed) {
545
612
  if (fs.existsSync(path.join(ROOT, claimed))) return true;
546
613
  if (!claimed.startsWith("src/")) return false;
547
- for (const workspace of ["apps", "packages"]) {
548
- const wsDir = path.join(ROOT, workspace);
549
- let entries;
550
- try { entries = fs.readdirSync(wsDir, { withFileTypes: true }); }
551
- catch (_e) { continue; }
552
- for (const entry of entries) {
553
- if (!entry.isDirectory()) continue;
554
- if (fs.existsSync(path.join(wsDir, entry.name, claimed))) return true;
555
- }
614
+ // v2.4.0: JS monorepo workspaces (apps/*, packages/*).
615
+ // v2.5.0: any first- or second-level module directory — Gradle/Maven
616
+ // multi-module (`api/src/main/java/...`, `servers/query/x/src/main/kotlin/...`)
617
+ // cite `src/...` relative to the module, exactly like JS workspaces do.
618
+ // Without this, widening SRC_PATH_RE to .java/.kt/.py would have turned
619
+ // every valid multi-module citation into a STALE_PATH false positive.
620
+ for (const moduleRoot of getModuleSrcRoots(ROOT)) {
621
+ if (fs.existsSync(path.join(moduleRoot, claimed))) return true;
556
622
  }
557
623
  return false;
558
624
  }
@@ -608,6 +674,9 @@ async function main() {
608
674
  if (seen.has(claimed)) continue;
609
675
  seen.add(claimed);
610
676
  if (hasPlaceholder(claimed)) continue;
677
+ // A dependency path is a reference to a library, not a claim about
678
+ // this repository's source tree.
679
+ if (/(^|\/)node_modules\//.test(claimed)) continue;
611
680
  pathClaimsChecked++;
612
681
  if (!resolvePathClaim(ROOT, claimed)) {
613
682
  pathClaimErrors++;