add-coder 0.3.37 → 0.3.38
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/README.en.md +16 -2
- package/README.md +18 -4
- package/dist/index.js +462 -84
- package/package.json +6 -2
- package/templates/.add-coder-src-hash.json +5 -5
- package/templates/core/scripts/db-ensure.sh +18 -2
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +7 -18
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +2 -1
- package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +6 -2
package/README.en.md
CHANGED
|
@@ -301,8 +301,22 @@ npx add-coder init
|
|
|
301
301
|
| Plan | Description |
|
|
302
302
|
|------|-------------|
|
|
303
303
|
| Demo Repo | A full example repository showcasing end-to-end closed-loop practice of Policy-Update-Loop and the Report system |
|
|
304
|
-
| MCP Restructure | MCP toolchain architecture upgrade, improving audit and gateway tool extensibility and standalone deployment capability |
|
|
305
|
-
| Memory Enhancement | ✅ **
|
|
304
|
+
| ~~MCP Restructure~~ | ✅ v0.2.9 MCP toolchain architecture upgrade, improving audit and gateway tool extensibility and standalone deployment capability |
|
|
305
|
+
| ~~Memory Enhancement~~ | ✅ **v0.3.35→v0.3.37 Governance memory**: idempotent evidence collection + deterministic waypoint recall + hybrid FTS×vector recall (RRF fusion + governed rerank) + Handoff Digest candidates + a ranking calibration foundation; native layer (SQLite FTS5 tables + triggers) auto-applied by `init`/`db-ensure` with `memory:reindex` self-heal; defaults to `shadow` mode (recall runs and is audited, not injected yet) |
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## 🙏 Acknowledgements
|
|
310
|
+
|
|
311
|
+
A paradigm is worth what people make of it — including the problems they find in it. The **contributor wall** lives in **[CONTRIBUTING.md §贡献者墙](./CONTRIBUTING.md#-贡献者墙)**; the item-by-item detail (in Chinese) lives in **[docs/ACKNOWLEDGEMENTS.md](./docs/ACKNOWLEDGEMENTS.md)**.
|
|
312
|
+
|
|
313
|
+
| Contributor | Contribution | What it left behind |
|
|
314
|
+
|-------------|--------------|---------------------|
|
|
315
|
+
| [@iopzhu](https://github.com/iopzhu) | Memory rotation on the `memory_cache` branch (`357b215`, 48 files / +5877 −33) | The retrieval and tooling backbone of the memory loop: `retrieval/pipeline`, RRF fusion, governed rerank, dual FTS channels, evidence/snapshot jobs, `tools/memory.ts` (609 lines) plus a 909-line recall evaluation set |
|
|
316
|
+
| [@Milkycoffees](https://github.com/Milkycoffees) | Ecosystem work: [`add-coder-flash`](https://www.npmjs.com/package/add-coder-flash) (npm `1.0.4`, 5 releases, plus a [public GitHub repo](https://github.com/Milkycoffees/add-coder-flash)) | A zero-dependency lightweight distribution: pure-`.mjs` hooks, no database required, assets laid down by `npm install`; auto-generates `.qoder-cn/mcp.json` and fed real constraints back (QoderCN config location, pnpm `allowBuilds`, MCP name length) |
|
|
317
|
+
| [@albertm88](https://github.com/albertm88) | Relentless issue reporting (#5–#7, #10–#20 — 14 issues, all closed) | Cross-session memory, 429 storms, Windows stability, native Codex adaptation, port drift, non-interactive hangs, first-run HITL approval… walking the edge cases so nobody else has to |
|
|
318
|
+
|
|
319
|
+
> Want on the wall? The criteria live in [CONTRIBUTING.md §贡献者墙](./CONTRIBUTING.md#-贡献者墙) — file an [issue](https://github.com/xiaomingming92/add-coder/issues) (reproducible problems, real logs, platform differences) or open a [PR](https://github.com/xiaomingming92/add-coder/pulls); both count as hard contribution.
|
|
306
320
|
|
|
307
321
|
---
|
|
308
322
|
<details id="chinese-readme">
|
package/README.md
CHANGED
|
@@ -499,10 +499,10 @@ Tasks (实验性) 双向 ✅ 已实现 长任务持久化
|
|
|
499
499
|
| 计划 | 说明 |备注|
|
|
500
500
|
|------|------|------|
|
|
501
501
|
| Demo 仓库演示 | 提供完整示例仓库,展示 Policy-Update-Loop 与 Report 体系的端到端闭环实践 |--|
|
|
502
|
-
| MCP
|
|
503
|
-
| Hook
|
|
502
|
+
| ~~MCP 能力重构~~ | ✅ v0.2.9 MCP 工具链架构升级,提升审计与门禁工具的可扩展性和独立部署能力 | 2026-07/23/add-coder-mcp-restructure-plan-v1.md |
|
|
503
|
+
| ~~Hook 通知升级~~ | ✅ v0.2.9 Hook 拦截事件 jsonl → fs.watch → record_dev_operation 落库 + Notification + 治理信号 | 2026-07/24/add-coder-hook-notify-upgrade-plan-v1.md |
|
|
504
504
|
| ide插件 | 解耦ADD范式代码和被治理项目的代码 |在做了,大家拭目以待吧,让编程更有趣,我的目标其实不在于IDE,我的工作顺手的事情 |
|
|
505
|
-
|
|
|
505
|
+
| ~~对话记忆增强~~ | ✅ **v0.3.35→v0.3.37 记忆闭环落地**:幂等采证 + 位点确定性召回 + FTS×向量混合召回(RRF 融合 + 治理重排)+ Handoff Digest 候选 + 排序权重校准基座;默认 `shadow` 模式(召回照跑照审计、暂不注入);v0.3.37 起原生层(SQLite FTS5 虚表 + 触发器)由 init/db-ensure 自动应用,`memory:reindex` 自助修复 | 门槛未下调:Hybrid MRR@5 0.4867 < 0.75,由校准线程以数据逼近 |
|
|
506
506
|
|
|
507
507
|
---
|
|
508
508
|
|
|
@@ -970,10 +970,24 @@ npx add-coder stack --clear # clear tech-stack setting (neutral)
|
|
|
970
970
|
| Demo Repo | Full example repository showcasing Policy-Update-Loop with Report system end-to-end | 📋 |
|
|
971
971
|
| ~~MCP Restructure~~ | ✅ v0.2.9 3,467-line monolith → 27 modules covering all six MCP primitives | 2026-07/23/... |
|
|
972
972
|
| ~~Hook Notification Upgrade~~ | ✅ v0.2.9 ~36 files: hook intercept → jsonl → fs.watch → record_dev_operation | 2026-07/24/... |
|
|
973
|
-
| Memory Enhancement |
|
|
973
|
+
| ~~Memory Enhancement~~ | ✅ v0.3.35→v0.3.37 **Governance memory**: idempotent evidence collection + deterministic waypoint recall + hybrid FTS×vector recall (RRF fusion + governed rerank) + Handoff Digest candidates + ranking-calibration foundation; SQLite FTS5 native layer auto-applied by `init`/`db-ensure` + `memory:reindex` self-heal; defaults to `shadow` mode | 2026-09/15/... |
|
|
974
974
|
|
|
975
975
|
---
|
|
976
976
|
|
|
977
977
|
My hometown
|
|
978
978
|
|
|
979
979
|
<img width="720" alt="hometown outside" src="https://github.com/user-attachments/assets/7aad93de-9cea-4194-b09d-cbd892d61cc2" />
|
|
980
|
+
|
|
981
|
+
---
|
|
982
|
+
|
|
983
|
+
## 🙏 致谢 / Acknowledgements
|
|
984
|
+
|
|
985
|
+
范式的价值来自把它用出问题的人。**贡献者墙**见 **[CONTRIBUTING.md §贡献者墙](./CONTRIBUTING.md#-贡献者墙)**,逐项明细与证据见 **[docs/ACKNOWLEDGEMENTS.md](./docs/ACKNOWLEDGEMENTS.md)**。
|
|
986
|
+
|
|
987
|
+
| 贡献者 | 贡献形态 | 留下了什么 |
|
|
988
|
+
|--------|---------|-----------|
|
|
989
|
+
| [@iopzhu](https://github.com/iopzhu) | 记忆轮动(`memory_cache` 分支 `357b215`,48 文件 / +5877 −33) | 记忆闭环的检索层与工具层主体:`retrieval/pipeline`、RRF 融合、治理重排、FTS 双通道、采证与快照作业、`tools/memory.ts`(609 行)+ 评测标注集(909 行) |
|
|
990
|
+
| [@Milkycoffees](https://github.com/Milkycoffees) | 生态贡献:[`add-coder-flash`](https://www.npmjs.com/package/add-coder-flash)(npm `1.0.4` / 5 个版本 + [GitHub 开源](https://github.com/Milkycoffees/add-coder-flash)) | 零依赖轻量发行版:hooks 纯 `.mjs`、不需要数据库,`npm install` 即铺设;自动生成 `.qoder-cn/mcp.json`,并回吐 QoderCN 加载位置、pnpm `allowBuilds`、MCP 名长度等真实约束 |
|
|
991
|
+
| [@albertm88](https://github.com/albertm88) | 持续输出 issue(#5–#7、#10–#20,14 条全部闭环) | 跨会话记忆、429 并发、Windows 稳定性、Codex 原生适配、端口漂移、非交互挂起、HITL 首次审批……把边界条件替所有人先踩了一遍 |
|
|
992
|
+
|
|
993
|
+
> 想上墙?上榜标准在 [CONTRIBUTING.md §贡献者墙](./CONTRIBUTING.md#-贡献者墙)——提 [Issue](https://github.com/xiaomingming92/add-coder/issues)(可复现的问题、实测日志、平台差异)或 [PR](https://github.com/xiaomingming92/add-coder/pulls) 都算硬贡献。
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/caijuehub/strategies/detect.strategy.ts
|
|
@@ -60,6 +60,24 @@ function magicDirFor(adapter) {
|
|
|
60
60
|
if (!m) throw new Error(`\u672A\u77E5 adapter: ${adapter}`);
|
|
61
61
|
return m;
|
|
62
62
|
}
|
|
63
|
+
function here() {
|
|
64
|
+
return import.meta.dirname;
|
|
65
|
+
}
|
|
66
|
+
function resolveMagicDir(cwd) {
|
|
67
|
+
const fromEnv = process.env.MAGIC_DIR;
|
|
68
|
+
if (fromEnv) return fromEnv;
|
|
69
|
+
const base = cwd ?? here();
|
|
70
|
+
const ide = detectIDE2(base);
|
|
71
|
+
if (ide !== "auto") {
|
|
72
|
+
const m = MAGIC_DIR_MAP[ide];
|
|
73
|
+
if (m && findUpSync(m, { cwd: base, type: "directory" })) return m;
|
|
74
|
+
}
|
|
75
|
+
for (const magic of Object.values(MAGIC_DIR_MAP)) {
|
|
76
|
+
const hit = findUpSync(magic, { cwd: base, type: "directory" });
|
|
77
|
+
if (hit) return magic;
|
|
78
|
+
}
|
|
79
|
+
return MAGIC_DIR_MAP.qoder;
|
|
80
|
+
}
|
|
63
81
|
|
|
64
82
|
// src/cli/config-loader.ts
|
|
65
83
|
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
@@ -200,7 +218,7 @@ async function selectFiles(projectRoot, files) {
|
|
|
200
218
|
process.stdout.write(renderList(items, selected, scrollIdx, VISIBLE));
|
|
201
219
|
}
|
|
202
220
|
draw();
|
|
203
|
-
return new Promise((
|
|
221
|
+
return new Promise((resolve14) => {
|
|
204
222
|
input.on("keypress", (_char, key) => {
|
|
205
223
|
if (!key) return;
|
|
206
224
|
if (key.name === "up") {
|
|
@@ -241,7 +259,7 @@ async function selectFiles(projectRoot, files) {
|
|
|
241
259
|
if (selected.has(idx)) result.set(relPath, content);
|
|
242
260
|
idx++;
|
|
243
261
|
}
|
|
244
|
-
|
|
262
|
+
resolve14(result);
|
|
245
263
|
});
|
|
246
264
|
});
|
|
247
265
|
}
|
|
@@ -1530,9 +1548,9 @@ async function injectPrisma2(projectRoot, options = {}) {
|
|
|
1530
1548
|
}
|
|
1531
1549
|
|
|
1532
1550
|
// src/cli/commands/init.ts
|
|
1533
|
-
import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as
|
|
1551
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as existsSync13, mkdirSync as mkdirSync6, copyFileSync as copyFileSync2, readdirSync as readdirSync5 } from "fs";
|
|
1534
1552
|
import { createHash } from "crypto";
|
|
1535
|
-
import { resolve as
|
|
1553
|
+
import { resolve as resolve9, dirname as dirname3, join as pathJoin } from "path";
|
|
1536
1554
|
import { homedir as homedir2 } from "os";
|
|
1537
1555
|
import { createConnection as createConnection2 } from "net";
|
|
1538
1556
|
|
|
@@ -1612,6 +1630,42 @@ async function ensureEmbeddingModel(options) {
|
|
|
1612
1630
|
return { status: "downloaded", model, cacheDir };
|
|
1613
1631
|
}
|
|
1614
1632
|
|
|
1633
|
+
// src/lib/memory-expected-state.ts
|
|
1634
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1635
|
+
import { join as join9, resolve as resolve8 } from "path";
|
|
1636
|
+
function expectedStateSqlRelPath(magicDir) {
|
|
1637
|
+
return join9(magicDir, "scripts", "mcp-server", "shared", "memory", "retrieval", "fts", "sqlite-fts5.sql");
|
|
1638
|
+
}
|
|
1639
|
+
function applyMemoryExpectedState(opts) {
|
|
1640
|
+
const { projectRoot, magicDir, engine } = opts;
|
|
1641
|
+
const relSql = expectedStateSqlRelPath(magicDir);
|
|
1642
|
+
const manualCmd = `npx prisma db execute --file ${relSql}`;
|
|
1643
|
+
if (engine !== "sqlite") return { applicable: false, ok: true, relSql, manualCmd };
|
|
1644
|
+
const absSql = resolve8(projectRoot, relSql);
|
|
1645
|
+
if (!existsSync12(absSql)) {
|
|
1646
|
+
return {
|
|
1647
|
+
applicable: true,
|
|
1648
|
+
ok: false,
|
|
1649
|
+
relSql,
|
|
1650
|
+
manualCmd,
|
|
1651
|
+
detail: `\u671F\u671B\u6001 SQL \u672A\u627E\u5230\uFF08${relSql}\uFF09\u2014\u2014\u8BF7\u5148\u6267\u884C add-coder sync \u540E\u91CD\u8DD1`
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
const pm = detectPm(projectRoot);
|
|
1655
|
+
const argv = pm === "pnpm" ? ["exec", "prisma", "db", "execute", "--file", absSql] : ["exec", "prisma", "--", "db", "execute", "--file", absSql];
|
|
1656
|
+
const run = opts.run ?? ((a, cwd) => runCommand(pm, a, { cwd, timeout: opts.timeoutMs ?? 6e4 }));
|
|
1657
|
+
try {
|
|
1658
|
+
const r = run(argv, projectRoot);
|
|
1659
|
+
if (r.status !== 0) {
|
|
1660
|
+
const why = (r.stderr || "").trim().split("\n").slice(0, 2).join(" | ");
|
|
1661
|
+
return { applicable: true, ok: false, relSql, manualCmd, detail: `\u9000\u51FA\u7801 ${r.status}${why ? `: ${why}` : ""}` };
|
|
1662
|
+
}
|
|
1663
|
+
return { applicable: true, ok: true, relSql, manualCmd };
|
|
1664
|
+
} catch (e) {
|
|
1665
|
+
return { applicable: true, ok: false, relSql, manualCmd, detail: e instanceof Error ? e.message : String(e) };
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1615
1669
|
// src/cli/commands/init.ts
|
|
1616
1670
|
var ADAPTER_RENDERERS = {
|
|
1617
1671
|
claude: renderAdapter,
|
|
@@ -1678,7 +1732,7 @@ async function handleCodexMcpConfig(options) {
|
|
|
1678
1732
|
console.log(`[dry-run] \u5C06\u5199\u5165 ${userConfigPath}`);
|
|
1679
1733
|
return;
|
|
1680
1734
|
}
|
|
1681
|
-
if (
|
|
1735
|
+
if (existsSync13(userConfigPath) && readFileSync10(userConfigPath, "utf-8").includes("mcp_servers.add_coder")) {
|
|
1682
1736
|
console.log(`\u26A0\uFE0F ${userConfigPath} \u5DF2\u5B58\u5728 mcp_servers.add_coder \u914D\u7F6E\uFF0C\u8DF3\u8FC7\uFF08\u907F\u514D\u91CD\u590D\uFF09`);
|
|
1683
1737
|
return;
|
|
1684
1738
|
}
|
|
@@ -1687,7 +1741,7 @@ async function handleCodexMcpConfig(options) {
|
|
|
1687
1741
|
console.log("\u5DF2\u53D6\u6D88\u5199\u5165");
|
|
1688
1742
|
return;
|
|
1689
1743
|
}
|
|
1690
|
-
if (
|
|
1744
|
+
if (existsSync13(userConfigPath)) {
|
|
1691
1745
|
const backup = `${userConfigPath}.bak-${Date.now()}`;
|
|
1692
1746
|
copyFileSync2(userConfigPath, backup);
|
|
1693
1747
|
console.log(`\u5DF2\u5907\u4EFD\u539F\u914D\u7F6E \u2192 ${backup}`);
|
|
@@ -1838,8 +1892,8 @@ networks:
|
|
|
1838
1892
|
`;
|
|
1839
1893
|
}
|
|
1840
1894
|
function writeSqliteExportScript(projectRoot, dryRun) {
|
|
1841
|
-
const scriptsDir =
|
|
1842
|
-
const scriptPath =
|
|
1895
|
+
const scriptsDir = resolve9(projectRoot, "scripts");
|
|
1896
|
+
const scriptPath = resolve9(scriptsDir, "export-db.ts");
|
|
1843
1897
|
const content = `import { PrismaClient } from "@prisma/client";
|
|
1844
1898
|
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
1845
1899
|
import { resolve } from "path";
|
|
@@ -1863,13 +1917,13 @@ main().catch((e) => { console.error(e); process.exit(1); });
|
|
|
1863
1917
|
console.log(`[dry-run] \u5C06\u5199\u5165 ${scriptPath}`);
|
|
1864
1918
|
return;
|
|
1865
1919
|
}
|
|
1866
|
-
if (!
|
|
1920
|
+
if (!existsSync13(scriptsDir)) mkdirSync6(scriptsDir, { recursive: true });
|
|
1867
1921
|
writeFileSync6(scriptPath, content, "utf-8");
|
|
1868
1922
|
console.log("\u5DF2\u751F\u6210 scripts/export-db.ts");
|
|
1869
1923
|
}
|
|
1870
1924
|
function injectDbExportScript(projectRoot, dryRun) {
|
|
1871
|
-
const pkgPath =
|
|
1872
|
-
if (!
|
|
1925
|
+
const pkgPath = resolve9(projectRoot, "package.json");
|
|
1926
|
+
if (!existsSync13(pkgPath)) return;
|
|
1873
1927
|
if (dryRun) {
|
|
1874
1928
|
console.log("[dry-run] \u5C06\u6CE8\u5165 db:export");
|
|
1875
1929
|
return;
|
|
@@ -1921,14 +1975,14 @@ function writeComposeEnv(ctx) {
|
|
|
1921
1975
|
if (db.engine !== "postgresql" || !db.container || db.container === "manual") return;
|
|
1922
1976
|
if (!db.reuseExisting) {
|
|
1923
1977
|
const composeName = db.container === "podman" ? "podman-compose.add.yml" : "docker-compose.add.yml";
|
|
1924
|
-
const composePath =
|
|
1925
|
-
if (!options.dryRun && (!
|
|
1978
|
+
const composePath = resolve9(projectRoot, composeName);
|
|
1979
|
+
if (!options.dryRun && (!existsSync13(composePath) || options.force)) {
|
|
1926
1980
|
writeFileSync6(composePath, composeContent(config.projectName || "add-project"), "utf-8");
|
|
1927
1981
|
console.log(`\u5DF2\u521B\u5EFA ${composeName}`);
|
|
1928
1982
|
}
|
|
1929
1983
|
}
|
|
1930
|
-
const devEnvPath =
|
|
1931
|
-
if (!options.dryRun &&
|
|
1984
|
+
const devEnvPath = resolve9(projectRoot, ".env.development");
|
|
1985
|
+
if (!options.dryRun && existsSync13(devEnvPath)) {
|
|
1932
1986
|
const existing = readFileSync10(devEnvPath, "utf-8");
|
|
1933
1987
|
if (!/^DATABASE_USER=/m.test(existing)) {
|
|
1934
1988
|
writeFileSync6(devEnvPath, existing + `
|
|
@@ -1975,8 +2029,8 @@ async function renderAndWrite(ctx) {
|
|
|
1975
2029
|
}
|
|
1976
2030
|
const reviewTargets = target === "codex" ? [magicDir] : [ADD_DIR, magicDir];
|
|
1977
2031
|
for (const d of reviewTargets) {
|
|
1978
|
-
const reviewsDir =
|
|
1979
|
-
if (!
|
|
2032
|
+
const reviewsDir = resolve9(projectRoot, d, "reviews");
|
|
2033
|
+
if (!existsSync13(reviewsDir)) {
|
|
1980
2034
|
if (dry) {
|
|
1981
2035
|
console.log(`[dry-run] \u5C06\u521B\u5EFA ${reviewsDir}/`);
|
|
1982
2036
|
} else {
|
|
@@ -1988,16 +2042,16 @@ async function renderAndWrite(ctx) {
|
|
|
1988
2042
|
const hashMap = {};
|
|
1989
2043
|
let npmVer = "";
|
|
1990
2044
|
try {
|
|
1991
|
-
npmVer = JSON.parse(readFileSync10(
|
|
2045
|
+
npmVer = JSON.parse(readFileSync10(resolve9(projectRoot, "node_modules", "add-coder", "templates", ".add-coder-src-hash.json"), "utf-8"))._version ?? "";
|
|
1992
2046
|
} catch {
|
|
1993
2047
|
}
|
|
1994
2048
|
for (const [rp, c] of allFiles) {
|
|
1995
2049
|
hashMap[rp] = createHash("sha256").update(c).digest("hex").slice(0, 8);
|
|
1996
2050
|
}
|
|
1997
|
-
const hashOut =
|
|
2051
|
+
const hashOut = resolve9(projectRoot, magicDir, ".add-coder-hash.json");
|
|
1998
2052
|
writeFileSync6(hashOut, JSON.stringify(hashMap, null, 2) + "\n", "utf-8");
|
|
1999
2053
|
if (npmVer) {
|
|
2000
|
-
writeFileSync6(
|
|
2054
|
+
writeFileSync6(resolve9(projectRoot, magicDir, ".add-coder-version"), npmVer + "\n", "utf-8");
|
|
2001
2055
|
}
|
|
2002
2056
|
console.log(`hash: ${Object.keys(hashMap).length} entries \u2192 ${magicDir}/.add-coder-hash.json`);
|
|
2003
2057
|
}
|
|
@@ -2008,7 +2062,7 @@ async function deployDatabase(ctx) {
|
|
|
2008
2062
|
if (options.dryRun) return null;
|
|
2009
2063
|
let fail = null;
|
|
2010
2064
|
if (db.engine === "postgresql" && db.container && db.container !== "manual") {
|
|
2011
|
-
const dbScript =
|
|
2065
|
+
const dbScript = resolve9(projectRoot, magicDir, "scripts", "db-ensure.sh");
|
|
2012
2066
|
const dbEnv = { ...process.env, DATABASE_USER: db.user, DATABASE_PASSWORD: db.password, DATABASE_PORT: db.port, PROJECT_NAME: config.projectName };
|
|
2013
2067
|
const mode = db.reuseExisting ? "manual" : db.container;
|
|
2014
2068
|
console.log(db.reuseExisting ? "\u590D\u7528\u5DF2\u6709 PostgreSQL ..." : `\u90E8\u7F72\u6570\u636E\u5E93 (${db.container}) ...`);
|
|
@@ -2026,8 +2080,8 @@ ${bashRun.stderr.trim().split("\n").slice(0, 5).join("\n")}` : ""}`;
|
|
|
2026
2080
|
}
|
|
2027
2081
|
}
|
|
2028
2082
|
if (db.engine === "postgresql" && db.container === "manual") {
|
|
2029
|
-
const dbScript =
|
|
2030
|
-
if (
|
|
2083
|
+
const dbScript = resolve9(projectRoot, magicDir, "scripts", "db-ensure.sh");
|
|
2084
|
+
if (existsSync13(dbScript)) {
|
|
2031
2085
|
try {
|
|
2032
2086
|
const bashRun = runCommand("bash", [dbScript, "postgresql", "manual"], { cwd: projectRoot, stdio: "inherit" });
|
|
2033
2087
|
if (bashRun.status !== 0) fail = `db-ensure.sh \u9000\u51FA\u7801: ${bashRun.status}${bashRun.stderr ? `
|
|
@@ -2062,6 +2116,14 @@ ${bashRun.stderr.trim().split("\n").slice(0, 5).join("\n")}` : ""}`;
|
|
|
2062
2116
|
injectDbExportScript(projectRoot, false);
|
|
2063
2117
|
try {
|
|
2064
2118
|
await injectPrisma2(projectRoot, { force: !!options.force, datasource: "sqlite" });
|
|
2119
|
+
const expected = applyMemoryExpectedState({ projectRoot, magicDir, engine: db.engine });
|
|
2120
|
+
if (expected.applicable && !expected.ok) {
|
|
2121
|
+
console.warn(`\u26A0\uFE0F \u8BB0\u5FC6 FTS5 \u671F\u671B\u6001\u5E94\u7528\u5931\u8D25\uFF08\u4E0D\u963B\u65AD init\uFF09\uFF1A${expected.detail ?? "\u672A\u77E5\u539F\u56E0"}`);
|
|
2122
|
+
console.warn(` \u540E\u7EED recall_memory \u4F1A\u62A5 sqlite-fts \u4E0D\u53EF\u7528\uFF1B\u624B\u5DE5\u6062\u590D: ${expected.manualCmd}`);
|
|
2123
|
+
console.warn(` \u6216\u8FD0\u884C: add-coder memory:reindex --apply`);
|
|
2124
|
+
} else if (expected.applicable) {
|
|
2125
|
+
console.log(`\u2705 \u8BB0\u5FC6 FTS5 \u671F\u671B\u6001\u5DF2\u5E94\u7528\uFF08${expected.relSql}\uFF09`);
|
|
2126
|
+
}
|
|
2065
2127
|
} catch (e) {
|
|
2066
2128
|
fail = `SQLite \u540C\u6B65\u5931\u8D25: ${e instanceof Error ? e.message : String(e)}`;
|
|
2067
2129
|
}
|
|
@@ -2072,17 +2134,17 @@ function deployDocs(ctx) {
|
|
|
2072
2134
|
const { projectRoot, options, config } = ctx;
|
|
2073
2135
|
if (options.dryRun) return;
|
|
2074
2136
|
const pn = config.projectName || "add-project";
|
|
2075
|
-
const docsBase =
|
|
2076
|
-
const groundingSrc =
|
|
2137
|
+
const docsBase = resolve9(projectRoot, "docs", pn, "knowledge");
|
|
2138
|
+
const groundingSrc = resolve9(import.meta.dirname, "../templates/core/templates");
|
|
2077
2139
|
for (const d of ["00-\u9700\u6C42", "01-\u67B6\u6784", "02-\u89C4\u8303"]) {
|
|
2078
|
-
const srcDir =
|
|
2079
|
-
const destDir =
|
|
2080
|
-
if (!
|
|
2081
|
-
if (!
|
|
2140
|
+
const srcDir = resolve9(groundingSrc, d);
|
|
2141
|
+
const destDir = resolve9(docsBase, d);
|
|
2142
|
+
if (!existsSync13(destDir)) mkdirSync6(destDir, { recursive: true });
|
|
2143
|
+
if (!existsSync13(srcDir)) continue;
|
|
2082
2144
|
for (const f of readdirSync5(srcDir)) {
|
|
2083
|
-
const src =
|
|
2084
|
-
const dest =
|
|
2085
|
-
if (
|
|
2145
|
+
const src = resolve9(srcDir, f);
|
|
2146
|
+
const dest = resolve9(destDir, f);
|
|
2147
|
+
if (existsSync13(dest)) continue;
|
|
2086
2148
|
try {
|
|
2087
2149
|
copyFileSync2(src, dest);
|
|
2088
2150
|
} catch {
|
|
@@ -2098,7 +2160,7 @@ function finalize(ctx, result, dbFail) {
|
|
|
2098
2160
|
return;
|
|
2099
2161
|
}
|
|
2100
2162
|
if (db.engine === "sqlite") console.log("\u6570\u636E\u5907\u4EFD: npm run db:export \u2192 data/exports/");
|
|
2101
|
-
const pkg = JSON.parse(readFileSync10(
|
|
2163
|
+
const pkg = JSON.parse(readFileSync10(resolve9(import.meta.dirname, "../package.json"), "utf-8"));
|
|
2102
2164
|
const peerNames = Object.keys(pkg.peerDependencies || {});
|
|
2103
2165
|
if (peerNames.length > 0) {
|
|
2104
2166
|
console.log(`
|
|
@@ -2131,8 +2193,8 @@ function finalize(ctx, result, dbFail) {
|
|
|
2131
2193
|
}
|
|
2132
2194
|
|
|
2133
2195
|
// src/cli/commands/sync.ts
|
|
2134
|
-
import { existsSync as
|
|
2135
|
-
import { resolve as
|
|
2196
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
|
|
2197
|
+
import { resolve as resolve10, dirname as dirname4 } from "path";
|
|
2136
2198
|
import { createHash as createHash2 } from "crypto";
|
|
2137
2199
|
|
|
2138
2200
|
// src/lib/path-normalize.ts
|
|
@@ -2205,7 +2267,7 @@ function resolvePatchConflictMode(yes = false, isTTY = Boolean(process.stdin.isT
|
|
|
2205
2267
|
}
|
|
2206
2268
|
function loadHashFile(root, magic) {
|
|
2207
2269
|
try {
|
|
2208
|
-
const raw = JSON.parse(readFileSync11(
|
|
2270
|
+
const raw = JSON.parse(readFileSync11(resolve10(root, magic, SYNC_CONFIG.HASH_OUTPUT_FILE), "utf-8"));
|
|
2209
2271
|
const normalized = {};
|
|
2210
2272
|
for (const [k, v] of Object.entries(raw)) normalized[normalizeRelPath(k)] = v;
|
|
2211
2273
|
return normalized;
|
|
@@ -2215,19 +2277,19 @@ function loadHashFile(root, magic) {
|
|
|
2215
2277
|
}
|
|
2216
2278
|
function loadVersionFile(root, magic) {
|
|
2217
2279
|
try {
|
|
2218
|
-
return readFileSync11(
|
|
2280
|
+
return readFileSync11(resolve10(root, magic, SYNC_CONFIG.VERSION_SENTINEL), "utf-8").trim();
|
|
2219
2281
|
} catch {
|
|
2220
2282
|
return "";
|
|
2221
2283
|
}
|
|
2222
2284
|
}
|
|
2223
2285
|
function saveVersionFile(root, magic, version2) {
|
|
2224
|
-
writeFileSync7(
|
|
2286
|
+
writeFileSync7(resolve10(root, magic, SYNC_CONFIG.VERSION_SENTINEL), version2 + "\n", "utf-8");
|
|
2225
2287
|
}
|
|
2226
2288
|
function loadTemplateVersion(root) {
|
|
2227
2289
|
const candidates = [
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2290
|
+
resolve10(root, "node_modules", "add-coder", "templates", ".add-coder-src-hash.json"),
|
|
2291
|
+
resolve10(import.meta.dirname, "../templates/.add-coder-src-hash.json"),
|
|
2292
|
+
resolve10(import.meta.dirname, "../../../templates/.add-coder-src-hash.json")
|
|
2231
2293
|
];
|
|
2232
2294
|
for (const candidate of candidates) {
|
|
2233
2295
|
try {
|
|
@@ -2240,7 +2302,7 @@ function loadTemplateVersion(root) {
|
|
|
2240
2302
|
function saveHashFile(root, magic, files) {
|
|
2241
2303
|
const m = {};
|
|
2242
2304
|
for (const [p, c] of files) m[p] = c;
|
|
2243
|
-
writeFileSync7(
|
|
2305
|
+
writeFileSync7(resolve10(root, magic, SYNC_CONFIG.HASH_OUTPUT_FILE), JSON.stringify(m, null, 2) + "\n", "utf-8");
|
|
2244
2306
|
}
|
|
2245
2307
|
function mergeFullHash(outHash, candidates, readDiskHash, preserveKeys = /* @__PURE__ */ new Set()) {
|
|
2246
2308
|
const finalHash = /* @__PURE__ */ new Map();
|
|
@@ -2341,8 +2403,8 @@ async function syncCommand(options = {}) {
|
|
|
2341
2403
|
let sameCount = 0;
|
|
2342
2404
|
for (const [relPath, content] of candidates) {
|
|
2343
2405
|
const key = normalizeRelPath(relPath);
|
|
2344
|
-
const absPath =
|
|
2345
|
-
if (!
|
|
2406
|
+
const absPath = resolve10(projectRoot, relPath);
|
|
2407
|
+
if (!existsSync14(absPath)) {
|
|
2346
2408
|
missingFiles.set(key, content);
|
|
2347
2409
|
} else if (establishBaseline) {
|
|
2348
2410
|
missingFiles.set(key, content);
|
|
@@ -2388,8 +2450,8 @@ async function syncCommand(options = {}) {
|
|
|
2388
2450
|
}
|
|
2389
2451
|
let finalHash = mergeFullHash(
|
|
2390
2452
|
outHash,
|
|
2391
|
-
[...candidates].map(([relPath]) => ({ relPath, absPath:
|
|
2392
|
-
(absPath) =>
|
|
2453
|
+
[...candidates].map(([relPath]) => ({ relPath, absPath: resolve10(projectRoot, relPath) })),
|
|
2454
|
+
(absPath) => existsSync14(absPath) ? hash8(readFileSync11(absPath, "utf-8")) : null,
|
|
2393
2455
|
unresolvedConflictKeys
|
|
2394
2456
|
);
|
|
2395
2457
|
if (target === "codex") {
|
|
@@ -2404,7 +2466,7 @@ async function syncCommand(options = {}) {
|
|
|
2404
2466
|
}
|
|
2405
2467
|
const missing = /* @__PURE__ */ new Map();
|
|
2406
2468
|
for (const [relPath, content] of allFiles) {
|
|
2407
|
-
if (!
|
|
2469
|
+
if (!existsSync14(resolve10(projectRoot, relPath))) {
|
|
2408
2470
|
missing.set(relPath, content);
|
|
2409
2471
|
}
|
|
2410
2472
|
}
|
|
@@ -2428,7 +2490,7 @@ async function syncCommand(options = {}) {
|
|
|
2428
2490
|
}
|
|
2429
2491
|
function printMigrateGuidance(targetPath, changed) {
|
|
2430
2492
|
const g = SYNC_PRISMA_CONFIG.POST_SYNC;
|
|
2431
|
-
const isManaged =
|
|
2493
|
+
const isManaged = existsSync14(resolve10(dirname4(targetPath), "migrations"));
|
|
2432
2494
|
const actions = isManaged ? g.MANAGED_ACTIONS : g.UNMANAGED_ACTIONS;
|
|
2433
2495
|
console.log(` \u25B6 \u5DF2\u5199\u5165 ${changed} \u5904\u53D8\u66F4\uFF0C${g.HEADER}`);
|
|
2434
2496
|
for (const a of actions) {
|
|
@@ -2467,7 +2529,7 @@ async function ensureAtlasCapability(projectRoot, yes = false) {
|
|
|
2467
2529
|
console.log(" \u6587\u6863: README.md \u2192 \u7AE0\u8282\u300CAtlas \u6570\u636E\u5E93\u540C\u6B65\u80FD\u529B\u300D/ DEVELOPMENT.md \xA7\u4E5D");
|
|
2468
2530
|
return;
|
|
2469
2531
|
}
|
|
2470
|
-
const pm =
|
|
2532
|
+
const pm = existsSync14(resolve10(projectRoot, "pnpm-lock.yaml")) ? "pnpm" : "npm";
|
|
2471
2533
|
console.log(` \u5B89\u88C5 ${pm} add -D @ariga/atlas ...`);
|
|
2472
2534
|
const r = runCommand(pm, ["add", "-D", "@ariga/atlas"], { cwd: projectRoot, timeout: 18e4 });
|
|
2473
2535
|
if (r.status !== 0) {
|
|
@@ -2479,8 +2541,8 @@ async function ensureAtlasCapability(projectRoot, yes = false) {
|
|
|
2479
2541
|
console.log(" \u2705 @ariga/atlas \u5DF2\u5B89\u88C5\uFF0CAtlas \u80FD\u529B\u5C31\u7EEA\uFF08node_modules/.bin/atlas\uFF09");
|
|
2480
2542
|
}
|
|
2481
2543
|
function checkHostAtlasSegment(projectRoot) {
|
|
2482
|
-
const script =
|
|
2483
|
-
if (!
|
|
2544
|
+
const script = resolve10(projectRoot, "scripts", "db-ensure.sh");
|
|
2545
|
+
if (!existsSync14(script)) return;
|
|
2484
2546
|
const content = readFileSync11(script, "utf-8");
|
|
2485
2547
|
if (content.includes("atlas_sync")) return;
|
|
2486
2548
|
console.warn(" \u26A0\uFE0F \u5BBF\u4E3B scripts/db-ensure.sh \u672A\u5305\u542B Atlas \u540C\u6B65\u6BB5\uFF08\u65E5\u5E38 db-ensure \u5C06\u7F3A\u5C11 ADD \u6CBB\u7406\u6A21\u578B\u540C\u6B65\uFF09");
|
|
@@ -2493,9 +2555,9 @@ function checkHostAtlasSegment(projectRoot) {
|
|
|
2493
2555
|
}
|
|
2494
2556
|
async function checkPrismaDiff(projectRoot, options) {
|
|
2495
2557
|
if (!options.patch) return;
|
|
2496
|
-
const basePath =
|
|
2497
|
-
const targetPath =
|
|
2498
|
-
if (!
|
|
2558
|
+
const basePath = resolve10(projectRoot, SYNC_PRISMA_CONFIG.BASE_SCHEMA);
|
|
2559
|
+
const targetPath = resolve10(projectRoot, SYNC_PRISMA_CONFIG.TARGET_PATTERN);
|
|
2560
|
+
if (!existsSync14(basePath)) {
|
|
2499
2561
|
console.log(`
|
|
2500
2562
|
\u26A0\uFE0F \u57FA\u51C6 schema \u4E0D\u5B58\u5728: ${basePath}`);
|
|
2501
2563
|
console.log(` \u8BF7\u786E\u4FDD add-coder \u5DF2\u6B63\u786E\u5B89\u88C5\u3002`);
|
|
@@ -2509,7 +2571,7 @@ async function checkPrismaDiff(projectRoot, options) {
|
|
|
2509
2571
|
\u2705 Prisma schema \u4E0E add-coder \u6807\u51C6\u4E00\u81F4\uFF0C\u65E0\u9700\u540C\u6B65\u3002`);
|
|
2510
2572
|
return;
|
|
2511
2573
|
}
|
|
2512
|
-
const targetExists =
|
|
2574
|
+
const targetExists = existsSync14(targetPath);
|
|
2513
2575
|
let modifiedCount = 0;
|
|
2514
2576
|
console.log(`
|
|
2515
2577
|
\u26A0\uFE0F Prisma schema \u5DEE\u5F02\u68C0\u6D4B:`);
|
|
@@ -2759,8 +2821,8 @@ function overwriteFieldLines(targetPath, basePath, modelName, conflicts) {
|
|
|
2759
2821
|
}
|
|
2760
2822
|
|
|
2761
2823
|
// src/cli/commands/status.ts
|
|
2762
|
-
import { existsSync as
|
|
2763
|
-
import { resolve as
|
|
2824
|
+
import { existsSync as existsSync15 } from "fs";
|
|
2825
|
+
import { resolve as resolve11 } from "path";
|
|
2764
2826
|
async function statusCommand() {
|
|
2765
2827
|
const projectRoot = process.cwd();
|
|
2766
2828
|
const config = await loadConfig(projectRoot);
|
|
@@ -2769,7 +2831,7 @@ async function statusCommand() {
|
|
|
2769
2831
|
const missing = [];
|
|
2770
2832
|
const present = [];
|
|
2771
2833
|
for (const [relPath] of coreFiles) {
|
|
2772
|
-
if (
|
|
2834
|
+
if (existsSync15(resolve11(projectRoot, relPath))) {
|
|
2773
2835
|
present.push(relPath);
|
|
2774
2836
|
} else {
|
|
2775
2837
|
missing.push(relPath);
|
|
@@ -2787,14 +2849,14 @@ async function statusCommand() {
|
|
|
2787
2849
|
}
|
|
2788
2850
|
|
|
2789
2851
|
// src/cli/commands/stack.ts
|
|
2790
|
-
import { readdirSync as readdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as
|
|
2791
|
-
import { resolve as
|
|
2852
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync16, mkdirSync as mkdirSync8 } from "fs";
|
|
2853
|
+
import { resolve as resolve12, join as join10 } from "path";
|
|
2792
2854
|
import { createHash as createHash3 } from "crypto";
|
|
2793
2855
|
var HASH_OUTPUT_FILE = ".add-coder-hash.json";
|
|
2794
2856
|
function hash82(c) {
|
|
2795
2857
|
return createHash3("sha256").update(c).digest("hex").slice(0, 8);
|
|
2796
2858
|
}
|
|
2797
|
-
function
|
|
2859
|
+
function resolveMagicDir2(projectRoot, specified) {
|
|
2798
2860
|
if (specified) {
|
|
2799
2861
|
return magicDirFor(specified);
|
|
2800
2862
|
}
|
|
@@ -2803,18 +2865,18 @@ function resolveMagicDir(projectRoot, specified) {
|
|
|
2803
2865
|
return magicDirFor(adapter);
|
|
2804
2866
|
}
|
|
2805
2867
|
function listCustomProfiles(projectRoot, magicDir, registryNames) {
|
|
2806
|
-
const dir =
|
|
2807
|
-
if (!
|
|
2868
|
+
const dir = resolve12(projectRoot, magicDir, "rules", "profiles");
|
|
2869
|
+
if (!existsSync16(dir)) return [];
|
|
2808
2870
|
return readdirSync6(dir).filter((f) => f.endsWith(".md") && !registryNames.has(f.replace(/-profile\.md$/, ""))).sort();
|
|
2809
2871
|
}
|
|
2810
2872
|
function profileExists(projectRoot, magicDir, name) {
|
|
2811
2873
|
const registry = loadProfileRegistry();
|
|
2812
2874
|
if (registry.some((p) => p.name === name)) return true;
|
|
2813
|
-
return
|
|
2875
|
+
return existsSync16(resolve12(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`));
|
|
2814
2876
|
}
|
|
2815
2877
|
function stackCommand(sub, name, options = {}) {
|
|
2816
2878
|
const projectRoot = process.cwd();
|
|
2817
|
-
const magicDir =
|
|
2879
|
+
const magicDir = resolveMagicDir2(projectRoot, options.adapter);
|
|
2818
2880
|
const registry = loadProfileRegistry();
|
|
2819
2881
|
if (options.clear) {
|
|
2820
2882
|
saveStack(projectRoot, magicDir, "");
|
|
@@ -2847,12 +2909,12 @@ function stackCommand(sub, name, options = {}) {
|
|
|
2847
2909
|
console.log("\u6280\u672F\u6808: \u672A\u8BBE\u7F6E\uFF08\u4E2D\u6027\uFF0C\u65E0\u6280\u672F\u6808\u5047\u8BBE\uFF09");
|
|
2848
2910
|
return;
|
|
2849
2911
|
}
|
|
2850
|
-
const profilePath =
|
|
2851
|
-
const stat =
|
|
2912
|
+
const profilePath = resolve12(projectRoot, magicDir, "rules", "profiles", `${current}-profile.md`);
|
|
2913
|
+
const stat = existsSync16(profilePath) ? readFileSync12(profilePath, "utf-8").length : 0;
|
|
2852
2914
|
console.log(`\u6280\u672F\u6808: ${current}`);
|
|
2853
|
-
console.log(`profile \u6587\u4EF6: ${profilePath}${
|
|
2915
|
+
console.log(`profile \u6587\u4EF6: ${profilePath}${existsSync16(profilePath) ? ` (${stat} \u5B57\u7B26)` : "\uFF08\u7F3A\u5931\uFF09"}`);
|
|
2854
2916
|
try {
|
|
2855
|
-
const raw = JSON.parse(readFileSync12(
|
|
2917
|
+
const raw = JSON.parse(readFileSync12(resolve12(projectRoot, magicDir, "stack.json"), "utf-8"));
|
|
2856
2918
|
if (raw.updatedAt) console.log(`\u66F4\u65B0\u65F6\u95F4: ${raw.updatedAt}`);
|
|
2857
2919
|
} catch {
|
|
2858
2920
|
}
|
|
@@ -2877,8 +2939,8 @@ function buildConfig(projectRoot, magicDir, stack) {
|
|
|
2877
2939
|
stack
|
|
2878
2940
|
};
|
|
2879
2941
|
try {
|
|
2880
|
-
const pkgPath =
|
|
2881
|
-
if (
|
|
2942
|
+
const pkgPath = resolve12(projectRoot, "package.json");
|
|
2943
|
+
if (existsSync16(pkgPath)) {
|
|
2882
2944
|
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
2883
2945
|
if (pkg.name) config.projectName = pkg.name;
|
|
2884
2946
|
}
|
|
@@ -2906,13 +2968,13 @@ function applyStack(projectRoot, magicDir, name) {
|
|
|
2906
2968
|
}
|
|
2907
2969
|
const hashMap = {};
|
|
2908
2970
|
try {
|
|
2909
|
-
Object.assign(hashMap, JSON.parse(readFileSync12(
|
|
2971
|
+
Object.assign(hashMap, JSON.parse(readFileSync12(resolve12(projectRoot, magicDir, HASH_OUTPUT_FILE), "utf-8")));
|
|
2910
2972
|
} catch {
|
|
2911
2973
|
}
|
|
2912
2974
|
let written = 0;
|
|
2913
2975
|
for (const [relPath, content] of stackRelated) {
|
|
2914
|
-
const targetPath =
|
|
2915
|
-
mkdirSync8(
|
|
2976
|
+
const targetPath = resolve12(projectRoot, relPath);
|
|
2977
|
+
mkdirSync8(join10(targetPath, ".."), { recursive: true });
|
|
2916
2978
|
writeFileSync8(targetPath, content, "utf-8");
|
|
2917
2979
|
hashMap[relPath] = hash82(content);
|
|
2918
2980
|
written++;
|
|
@@ -2923,24 +2985,331 @@ function applyStack(projectRoot, magicDir, name) {
|
|
|
2923
2985
|
};
|
|
2924
2986
|
const registry = loadProfileRegistry();
|
|
2925
2987
|
const isBuiltin = registry.some((p) => p.name === name);
|
|
2926
|
-
const profilePathMagic =
|
|
2927
|
-
const profilePathAdd =
|
|
2928
|
-
const projectRulesPath =
|
|
2929
|
-
const projectRulesContent =
|
|
2988
|
+
const profilePathMagic = resolve12(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`);
|
|
2989
|
+
const profilePathAdd = resolve12(projectRoot, ADD_DIR, "rules", "profiles", `${name}-profile.md`);
|
|
2990
|
+
const projectRulesPath = resolve12(projectRoot, magicDir, "rules", "project_rules.md");
|
|
2991
|
+
const projectRulesContent = existsSync16(projectRulesPath) ? readFileSync12(projectRulesPath, "utf-8") : "";
|
|
2930
2992
|
if (written === 0) fail(`\u672A\u6E32\u67D3\u4EFB\u4F55 stack \u76F8\u5173\u6587\u4EF6\uFF08${name}\uFF09\u2014\u2014Windows \u8DEF\u5F84\u5339\u914D\u5931\u6548\u9057\u7559\u95EE\u9898`);
|
|
2931
|
-
if (magicDir !== magicDirFor("codex") && isBuiltin && !
|
|
2932
|
-
if (!
|
|
2993
|
+
if (magicDir !== magicDirFor("codex") && isBuiltin && !existsSync16(profilePathAdd)) fail(`profile \u672A\u5199\u5165 ${ADD_DIR}: ${profilePathAdd}`);
|
|
2994
|
+
if (!existsSync16(profilePathMagic)) fail(`profile \u672A\u5199\u5165 ${magicDir}: ${profilePathMagic}`);
|
|
2933
2995
|
if (!projectRulesContent.includes("**\u5F53\u524D\u6280\u672F\u6808**") || !projectRulesContent.includes(name)) fail(`project_rules.md \u672A\u5305\u542B ${name} \u5F15\u7528`);
|
|
2934
|
-
writeFileSync8(
|
|
2996
|
+
writeFileSync8(resolve12(projectRoot, magicDir, HASH_OUTPUT_FILE), JSON.stringify(hashMap, null, 2) + "\n", "utf-8");
|
|
2935
2997
|
console.log(`\u2705 \u6280\u672F\u6808\u5DF2\u8BBE\u7F6E\u4E3A ${name}`);
|
|
2936
2998
|
console.log(` ${magicDir}/stack.json \u2192 ${name}`);
|
|
2937
|
-
console.log(` ${magicDir}/rules/profiles/${name}-profile.md ${
|
|
2999
|
+
console.log(` ${magicDir}/rules/profiles/${name}-profile.md ${existsSync16(resolve12(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`)) ? "\u5DF2\u5C31\u4F4D" : "\uFF08\u81EA\u5B9A\u4E49 profile\uFF0C\u9879\u76EE\u4FA7\u6587\u4EF6\uFF09"}`);
|
|
2938
3000
|
console.log(` project_rules.md \u5F15\u7528\u884C\u5DF2\u66F4\u65B0 + hash \u5DF2\u5237\u65B0\uFF08${written} \u4E2A\u6587\u4EF6\uFF09`);
|
|
2939
3001
|
}
|
|
2940
3002
|
|
|
3003
|
+
// src/lib/memory-fts-objects.ts
|
|
3004
|
+
var PG_FTS_OBJECTS = [
|
|
3005
|
+
{ name: "pg_trgm", kind: "extension", ddl: `CREATE EXTENSION IF NOT EXISTS pg_trgm;` },
|
|
3006
|
+
{
|
|
3007
|
+
name: "AddMemory_topic_trgm_idx",
|
|
3008
|
+
kind: "index",
|
|
3009
|
+
ddl: `CREATE INDEX IF NOT EXISTS "AddMemory_topic_trgm_idx" ON "public"."AddMemory" USING GIN ("topic" gin_trgm_ops);`
|
|
3010
|
+
},
|
|
3011
|
+
{
|
|
3012
|
+
name: "AddMemory_content_trgm_idx",
|
|
3013
|
+
kind: "index",
|
|
3014
|
+
ddl: `CREATE INDEX IF NOT EXISTS "AddMemory_content_trgm_idx" ON "public"."AddMemory" USING GIN ("content" gin_trgm_ops);`
|
|
3015
|
+
},
|
|
3016
|
+
{
|
|
3017
|
+
name: "AddMemoryEvidence_excerpt_trgm_idx",
|
|
3018
|
+
kind: "index",
|
|
3019
|
+
ddl: `CREATE INDEX IF NOT EXISTS "AddMemoryEvidence_excerpt_trgm_idx" ON "public"."AddMemoryEvidence" USING GIN ("excerpt" gin_trgm_ops);`
|
|
3020
|
+
}
|
|
3021
|
+
];
|
|
3022
|
+
var SQLITE_FTS_OBJECTS = [
|
|
3023
|
+
{
|
|
3024
|
+
name: "add_memory_fts",
|
|
3025
|
+
kind: "table",
|
|
3026
|
+
ddl: `CREATE VIRTUAL TABLE IF NOT EXISTS add_memory_fts USING fts5(memory_id UNINDEXED, topic, content, tokenize = 'trigram');`
|
|
3027
|
+
},
|
|
3028
|
+
{
|
|
3029
|
+
name: "add_memory_fts_ai",
|
|
3030
|
+
kind: "trigger",
|
|
3031
|
+
ddl: `CREATE TRIGGER IF NOT EXISTS add_memory_fts_ai AFTER INSERT ON "AddMemory" BEGIN INSERT INTO add_memory_fts(memory_id, topic, content) VALUES (new.id, new.topic, new.content); END;`
|
|
3032
|
+
},
|
|
3033
|
+
{
|
|
3034
|
+
name: "add_memory_fts_au",
|
|
3035
|
+
kind: "trigger",
|
|
3036
|
+
ddl: `CREATE TRIGGER IF NOT EXISTS add_memory_fts_au AFTER UPDATE ON "AddMemory" BEGIN DELETE FROM add_memory_fts WHERE memory_id = old.id; INSERT INTO add_memory_fts(memory_id, topic, content) VALUES (new.id, new.topic, new.content); END;`
|
|
3037
|
+
},
|
|
3038
|
+
{
|
|
3039
|
+
name: "add_memory_fts_ad",
|
|
3040
|
+
kind: "trigger",
|
|
3041
|
+
ddl: `CREATE TRIGGER IF NOT EXISTS add_memory_fts_ad AFTER DELETE ON "AddMemory" BEGIN DELETE FROM add_memory_fts WHERE memory_id = old.id; END;`
|
|
3042
|
+
}
|
|
3043
|
+
];
|
|
3044
|
+
function resolveFtsObjects(backend) {
|
|
3045
|
+
return backend === "postgres" ? PG_FTS_OBJECTS : SQLITE_FTS_OBJECTS;
|
|
3046
|
+
}
|
|
3047
|
+
function detectBackend(databaseUrl) {
|
|
3048
|
+
const url = (databaseUrl ?? "").trim().toLowerCase();
|
|
3049
|
+
if (url.startsWith("postgres://") || url.startsWith("postgresql://")) return "postgres";
|
|
3050
|
+
if (url.startsWith("sqlite:") || url.startsWith("file:") || url.endsWith(".db") || url.endsWith(".sqlite")) {
|
|
3051
|
+
return "sqlite";
|
|
3052
|
+
}
|
|
3053
|
+
throw new Error(`\u65E0\u6CD5\u8BC6\u522B\u7684 DATABASE_URL \u540E\u7AEF: ${databaseUrl}`);
|
|
3054
|
+
}
|
|
3055
|
+
var PG_MISSING_MARKER = "ADDCODER_FTS_MISSING:";
|
|
3056
|
+
function renderPgProbeSql(expected) {
|
|
3057
|
+
const values = expected.map((s) => ` ('${s.name.replace(/'/g, "''")}', '${s.kind === "extension" ? "extension" : "relation"}')`).join(",\n");
|
|
3058
|
+
return `-- \u7531 add-coder memory:reindex \u751F\u6210\uFF08\u771F\u6E90\uFF1Asrc/lib/memory-fts-objects.ts \u7684 PG_FTS_OBJECTS\uFF09
|
|
3059
|
+
DO $$
|
|
3060
|
+
DECLARE missing text;
|
|
3061
|
+
BEGIN
|
|
3062
|
+
SELECT string_agg(n.name, ',') INTO missing
|
|
3063
|
+
FROM (VALUES
|
|
3064
|
+
${values}
|
|
3065
|
+
) AS n(name, kind)
|
|
3066
|
+
WHERE NOT (
|
|
3067
|
+
(n.kind = 'extension' AND EXISTS (SELECT 1 FROM pg_extension e WHERE e.extname = n.name))
|
|
3068
|
+
OR (n.kind = 'relation' AND EXISTS (SELECT 1 FROM pg_indexes i WHERE i.indexname = n.name))
|
|
3069
|
+
);
|
|
3070
|
+
IF missing IS NOT NULL THEN
|
|
3071
|
+
RAISE EXCEPTION '${PG_MISSING_MARKER} %', missing;
|
|
3072
|
+
END IF;
|
|
3073
|
+
END $$;
|
|
3074
|
+
`;
|
|
3075
|
+
}
|
|
3076
|
+
function parsePgProbeMissing(stderr, expected) {
|
|
3077
|
+
const at = (stderr ?? "").indexOf(PG_MISSING_MARKER);
|
|
3078
|
+
if (at < 0) return [];
|
|
3079
|
+
const tail = stderr.slice(at + PG_MISSING_MARKER.length);
|
|
3080
|
+
const known = new Set(expected.map((s) => s.name));
|
|
3081
|
+
return tail.split(/[\s"',\n]+/).map((s) => s.trim()).filter((s) => known.has(s));
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
// src/lib/memory-fts-runtime.ts
|
|
3085
|
+
import { existsSync as existsSync17, mkdtempSync, readFileSync as readFileSync13, rmSync as rmSync2, writeFileSync as writeFileSync9 } from "fs";
|
|
3086
|
+
import { createRequire } from "module";
|
|
3087
|
+
import { tmpdir } from "os";
|
|
3088
|
+
import { isAbsolute, join as join11, resolve as resolve13 } from "path";
|
|
3089
|
+
var requireBuiltin = createRequire(import.meta.url);
|
|
3090
|
+
function loadNodeSqlite() {
|
|
3091
|
+
return requireBuiltin("node:sqlite");
|
|
3092
|
+
}
|
|
3093
|
+
async function probeVia(adapter, expected) {
|
|
3094
|
+
const specs = expected ?? resolveFtsObjects(adapter.backend);
|
|
3095
|
+
const existing = await adapter.listExisting();
|
|
3096
|
+
const missing = specs.filter((s) => !existing.has(s.name)).map((s) => s.name);
|
|
3097
|
+
const present = specs.length - missing.length;
|
|
3098
|
+
return {
|
|
3099
|
+
backend: adapter.backend,
|
|
3100
|
+
source: adapter.source,
|
|
3101
|
+
missing,
|
|
3102
|
+
rebuilt: [],
|
|
3103
|
+
present,
|
|
3104
|
+
total: specs.length,
|
|
3105
|
+
progress: specs.length === 0 ? 100 : Math.round(present / specs.length * 100)
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
async function reindexVia(adapter, expected) {
|
|
3109
|
+
const specs = expected ?? resolveFtsObjects(adapter.backend);
|
|
3110
|
+
const first = await probeVia(adapter, specs);
|
|
3111
|
+
const rebuilt = [];
|
|
3112
|
+
for (const name of first.missing) {
|
|
3113
|
+
const spec = specs.find((s) => s.name === name);
|
|
3114
|
+
if (!spec) continue;
|
|
3115
|
+
await adapter.apply(spec.ddl);
|
|
3116
|
+
rebuilt.push(spec.name);
|
|
3117
|
+
}
|
|
3118
|
+
const after = await probeVia(adapter, specs);
|
|
3119
|
+
return { ...after, rebuilt };
|
|
3120
|
+
}
|
|
3121
|
+
function sqlitePathCandidates(databaseUrl, projectRoot) {
|
|
3122
|
+
const raw = (databaseUrl ?? "").replace(/^file:/, "").replace(/^sqlite:/, "").split("?")[0];
|
|
3123
|
+
if (!raw) return [];
|
|
3124
|
+
if (isAbsolute(raw)) return [raw];
|
|
3125
|
+
return [resolve13(projectRoot, raw), resolve13(projectRoot, "prisma", raw)];
|
|
3126
|
+
}
|
|
3127
|
+
function createSqliteAdapter(opts) {
|
|
3128
|
+
const candidates = opts.dbPath ? [opts.dbPath] : sqlitePathCandidates(opts.databaseUrl, opts.projectRoot);
|
|
3129
|
+
const dbPath = candidates.find((p) => existsSync17(p));
|
|
3130
|
+
const source = dbPath ?? `${opts.databaseUrl}\uFF08\u672A\u627E\u5230\u5E93\u6587\u4EF6\uFF0C\u5DF2\u8BD5\uFF1A${candidates.join(" / ")}\uFF09`;
|
|
3131
|
+
function withDb(fn) {
|
|
3132
|
+
if (!dbPath) {
|
|
3133
|
+
throw new Error(
|
|
3134
|
+
`SQLite \u5E93\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${opts.databaseUrl}\u3002\u5148\u8FD0\u884C add-coder init\uFF08\u6216\u786E\u8BA4 .env.development \u7684 DATABASE_URL\uFF09\u2014\u2014\u5DF2\u8BD5\uFF1A${candidates.join(" / ")}`
|
|
3135
|
+
);
|
|
3136
|
+
}
|
|
3137
|
+
let sqlite;
|
|
3138
|
+
try {
|
|
3139
|
+
sqlite = loadNodeSqlite();
|
|
3140
|
+
} catch (e) {
|
|
3141
|
+
throw new Error(
|
|
3142
|
+
`\u5F53\u524D Node \u4E0D\u652F\u6301 node:sqlite\uFF08\u9700 \u226522.5\uFF0C\u5B9E\u6D4B ${process.version}\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}\uFF1B\u53EF\u6539\u7528 SQL \u6587\u4EF6\u624B\u5DE5\u5E94\u7528\uFF08npx prisma db execute --file <magicDir>/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql --schema prisma/schema.prisma\uFF09`
|
|
3143
|
+
);
|
|
3144
|
+
}
|
|
3145
|
+
const db = new sqlite.DatabaseSync(dbPath);
|
|
3146
|
+
try {
|
|
3147
|
+
return fn(db);
|
|
3148
|
+
} finally {
|
|
3149
|
+
db.close();
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
return {
|
|
3153
|
+
backend: "sqlite",
|
|
3154
|
+
source,
|
|
3155
|
+
listExisting: () => Promise.resolve().then(
|
|
3156
|
+
() => withDb((db) => {
|
|
3157
|
+
const rows = db.prepare("SELECT name FROM sqlite_master WHERE type IN ('table','trigger')").all();
|
|
3158
|
+
return new Set(rows.map((r) => r.name));
|
|
3159
|
+
})
|
|
3160
|
+
),
|
|
3161
|
+
apply: (ddl) => Promise.resolve().then(
|
|
3162
|
+
() => withDb((db) => {
|
|
3163
|
+
db.exec(ddl);
|
|
3164
|
+
})
|
|
3165
|
+
)
|
|
3166
|
+
};
|
|
3167
|
+
}
|
|
3168
|
+
function readPrismaDatasourceProvider(projectRoot) {
|
|
3169
|
+
for (const rel of ["prisma/schema.prisma", "prisma/add.prisma"]) {
|
|
3170
|
+
const p = resolve13(projectRoot, rel);
|
|
3171
|
+
if (!existsSync17(p)) continue;
|
|
3172
|
+
const m = readFileSync13(p, "utf-8").match(/datasource\s+\w+\s*\{[\s\S]*?provider\s*=\s*"([^"]+)"/);
|
|
3173
|
+
if (m) return m[1];
|
|
3174
|
+
}
|
|
3175
|
+
return null;
|
|
3176
|
+
}
|
|
3177
|
+
function backendForProvider(provider) {
|
|
3178
|
+
const p = provider.trim().toLowerCase();
|
|
3179
|
+
if (p === "postgresql" || p === "postgres") return "postgres";
|
|
3180
|
+
if (p === "sqlite") return "sqlite";
|
|
3181
|
+
throw new Error(`\u4E0D\u652F\u6301\u7684 datasource provider: ${provider}\uFF08\u672C\u547D\u4EE4\u4EC5\u652F\u6301 postgresql / sqlite\uFF09`);
|
|
3182
|
+
}
|
|
3183
|
+
function resolveProjectDatabaseUrl(projectRoot, env = process.env) {
|
|
3184
|
+
if (env.DATABASE_URL) return env.DATABASE_URL;
|
|
3185
|
+
for (const rel of [".env.development", ".env"]) {
|
|
3186
|
+
const p = resolve13(projectRoot, rel);
|
|
3187
|
+
if (!existsSync17(p)) continue;
|
|
3188
|
+
const m = readFileSync13(p, "utf-8").match(/^\s*DATABASE_URL\s*=\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
3189
|
+
if (m) return m[1].trim();
|
|
3190
|
+
}
|
|
3191
|
+
return null;
|
|
3192
|
+
}
|
|
3193
|
+
function createPrismaCliAdapter(opts) {
|
|
3194
|
+
const expected = opts.expected ?? PG_FTS_OBJECTS;
|
|
3195
|
+
const pm = detectPm(opts.projectRoot);
|
|
3196
|
+
const base = pm === "pnpm" ? ["exec", "prisma"] : ["exec", "prisma", "--"];
|
|
3197
|
+
const run = opts.run ?? ((argv, cwd) => runCommand(pm, argv, { cwd, timeout: opts.timeoutMs ?? 12e4 }));
|
|
3198
|
+
const source = opts.describeSource ?? "prisma db execute\uFF08\u9879\u76EE DATABASE_URL\uFF09";
|
|
3199
|
+
function executeSql(sql, tag) {
|
|
3200
|
+
const dir = mkdtempSync(join11(tmpdir(), "add-coder-fts-"));
|
|
3201
|
+
const file = join11(dir, `${tag}.sql`);
|
|
3202
|
+
writeFileSync9(file, sql, "utf-8");
|
|
3203
|
+
try {
|
|
3204
|
+
const r = run([...base, "db", "execute", "--file", file], opts.projectRoot);
|
|
3205
|
+
return { status: r.status, stderr: r.stderr ?? "" };
|
|
3206
|
+
} finally {
|
|
3207
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
return {
|
|
3211
|
+
backend: "postgres",
|
|
3212
|
+
source,
|
|
3213
|
+
listExisting: () => {
|
|
3214
|
+
const r = executeSql(renderPgProbeSql(expected), "probe");
|
|
3215
|
+
if (r.status === 0) return Promise.resolve(new Set(expected.map((s) => s.name)));
|
|
3216
|
+
const missing = parsePgProbeMissing(r.stderr, expected);
|
|
3217
|
+
if (missing.length > 0) {
|
|
3218
|
+
return Promise.resolve(new Set(expected.map((s) => s.name).filter((n) => !missing.includes(n))));
|
|
3219
|
+
}
|
|
3220
|
+
return Promise.reject(
|
|
3221
|
+
new Error(
|
|
3222
|
+
`PG FTS \u63A2\u6D4B\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${r.status}\uFF09\uFF1A${r.stderr.trim().split("\n").slice(0, 3).join(" | ") || "\u65E0 stderr"}`
|
|
3223
|
+
)
|
|
3224
|
+
);
|
|
3225
|
+
},
|
|
3226
|
+
apply: (ddl) => {
|
|
3227
|
+
const r = executeSql(`${ddl}
|
|
3228
|
+
`, "apply");
|
|
3229
|
+
if (r.status === 0) return Promise.resolve();
|
|
3230
|
+
return Promise.reject(
|
|
3231
|
+
new Error(
|
|
3232
|
+
`PG FTS \u671F\u671B\u6001\u5E94\u7528\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${r.status}\uFF09\uFF1A${r.stderr.trim().split("\n").slice(0, 3).join(" | ") || "\u65E0 stderr"}`
|
|
3233
|
+
)
|
|
3234
|
+
);
|
|
3235
|
+
}
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
// src/cli/commands/memory-reindex.ts
|
|
3240
|
+
function resolveAdapter3(projectRoot, options, deps) {
|
|
3241
|
+
const databaseUrl = resolveProjectDatabaseUrl(projectRoot, deps.env ?? process.env);
|
|
3242
|
+
let backend;
|
|
3243
|
+
if (options.backend) {
|
|
3244
|
+
backend = options.backend;
|
|
3245
|
+
} else {
|
|
3246
|
+
const provider = readPrismaDatasourceProvider(projectRoot);
|
|
3247
|
+
if (provider) backend = backendForProvider(provider);
|
|
3248
|
+
else if (databaseUrl) backend = detectBackend(databaseUrl);
|
|
3249
|
+
else {
|
|
3250
|
+
throw new Error(
|
|
3251
|
+
"\u65E0\u6CD5\u5224\u5B9A\u540E\u7AEF\uFF1A\u672A\u6307\u5B9A --backend\uFF0C\u4E14\u9879\u76EE\u5185\u65E2\u65E0 prisma/schema.prisma \u7684 datasource \u58F0\u660E\uFF0C\u4E5F\u65E0 .env.development \u7684 DATABASE_URL"
|
|
3252
|
+
);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
const magicDir = resolveMagicDir(projectRoot);
|
|
3256
|
+
if (backend === "sqlite") {
|
|
3257
|
+
if (!databaseUrl) {
|
|
3258
|
+
throw new Error(`SQLite \u540E\u7AEF\u9700\u8981 DATABASE_URL\uFF08.env.development \u6216\u8FDB\u7A0B\u73AF\u5883\uFF09\uFF1BmagicDir=${magicDir}`);
|
|
3259
|
+
}
|
|
3260
|
+
return createSqliteAdapter({ databaseUrl, projectRoot });
|
|
3261
|
+
}
|
|
3262
|
+
return createPrismaCliAdapter({
|
|
3263
|
+
projectRoot,
|
|
3264
|
+
run: deps.run,
|
|
3265
|
+
describeSource: databaseUrl ? `prisma db execute\uFF08DATABASE_URL=${maskUrl(databaseUrl)}\uFF09` : "prisma db execute\uFF08schema \u5185 env\uFF09"
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
function maskUrl(url) {
|
|
3269
|
+
return url.replace(/:\/\/([^:/@]+):[^@]*@/, "://$1:***@");
|
|
3270
|
+
}
|
|
3271
|
+
function printReport(report, mode, io) {
|
|
3272
|
+
const head = mode === "apply" ? "\u8BB0\u5FC6 FTS \u671F\u671B\u6001 \xB7 \u91CD\u5EFA" : "\u8BB0\u5FC6 FTS \u671F\u671B\u6001 \xB7 \u63A2\u6D4B";
|
|
3273
|
+
io.log(`${head}\uFF08backend=${report.backend}\uFF0Csource=${report.source}\uFF09`);
|
|
3274
|
+
io.log(` \u5B8C\u6210\u5EA6 ${report.present}/${report.total}\uFF08${report.progress}%\uFF09`);
|
|
3275
|
+
if (mode === "apply" && report.rebuilt.length > 0) {
|
|
3276
|
+
io.log(` \u672C\u6B21\u91CD\u5EFA\uFF08${report.rebuilt.length}\uFF09\uFF1A${report.rebuilt.join(", ")}`);
|
|
3277
|
+
}
|
|
3278
|
+
if (report.missing.length > 0) {
|
|
3279
|
+
io.log(` \u7F3A\u5931\uFF08${report.missing.length}\uFF09\uFF1A${report.missing.join(", ")}`);
|
|
3280
|
+
io.log(mode === "apply" ? " \u274C \u91CD\u5EFA\u540E\u4ECD\u7F3A\u5931\u2014\u2014\u8BF7\u68C0\u67E5 DDL \u662F\u5426\u88AB\u62D2\uFF08\u6743\u9650/\u6269\u5C55\u4E0D\u53EF\u7528\uFF09" : " \u2192 \u4FEE\u590D\uFF1Aadd-coder memory:reindex --apply");
|
|
3281
|
+
} else {
|
|
3282
|
+
io.log(" \u2705 \u671F\u671B\u6001\u5B8C\u6574");
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
async function memoryReindexCommand(options, deps = {}) {
|
|
3286
|
+
const io = deps.io ?? console;
|
|
3287
|
+
const projectRoot = options.cwd ?? process.cwd();
|
|
3288
|
+
if (options.apply && options.probe) {
|
|
3289
|
+
io.error(JSON.stringify({ ok: false, mode: "n/a", reason: "flags-conflict", detail: "--probe \u4E0E --apply \u4E92\u65A5\uFF08\u9ED8\u8BA4 probe\uFF09", exitCode: 2 }));
|
|
3290
|
+
return 2;
|
|
3291
|
+
}
|
|
3292
|
+
const mode = options.apply ? "apply" : "probe";
|
|
3293
|
+
try {
|
|
3294
|
+
const adapter = resolveAdapter3(projectRoot, options, deps);
|
|
3295
|
+
const report = mode === "apply" ? await reindexVia(adapter) : await probeVia(adapter);
|
|
3296
|
+
if (options.json) io.log(JSON.stringify(report, null, 2));
|
|
3297
|
+
else printReport(report, mode, io);
|
|
3298
|
+
if (mode === "probe") return 0;
|
|
3299
|
+
return report.missing.length > 0 ? 1 : 0;
|
|
3300
|
+
} catch (e) {
|
|
3301
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
3302
|
+
const context = { ok: false, mode, backend: options.backend ?? "auto", exitCode: 2, error: detail };
|
|
3303
|
+
io.error(
|
|
3304
|
+
options.json ? JSON.stringify(context) : `\u8BB0\u5FC6 FTS ${mode === "apply" ? "\u91CD\u5EFA" : "\u63A2\u6D4B"}\u5931\u8D25\uFF1A${detail}\uFF08backend=${context.backend}, exitCode=2\uFF09`
|
|
3305
|
+
);
|
|
3306
|
+
return 2;
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
|
|
2941
3310
|
// src/cli/index.ts
|
|
2942
3311
|
var { version } = JSON.parse(
|
|
2943
|
-
|
|
3312
|
+
readFileSync14(new URL("../package.json", import.meta.url), "utf-8")
|
|
2944
3313
|
);
|
|
2945
3314
|
async function modelDownloadCommand(options) {
|
|
2946
3315
|
try {
|
|
@@ -2962,4 +3331,13 @@ program.command("stack").description("\u7BA1\u7406\u6280\u672F\u6808\u7EA6\u675F
|
|
|
2962
3331
|
(sub, name, options) => stackCommand(sub, name, options)
|
|
2963
3332
|
);
|
|
2964
3333
|
program.command("model:download").description("\u9884\u4E0B\u8F7D embedding \u6A21\u578B\uFF08\u9996\u6B21 DPS \u8C03\u7528\u4F1A\u81EA\u52A8\u4E0B\u8F7D\uFF0C\u672C\u547D\u4EE4\u63D0\u524D\u62C9\u53D6\uFF09").option("--force", "\u5F3A\u5236\u91CD\u65B0\u4E0B\u8F7D\uFF08\u5373\u4F7F\u7F13\u5B58\u5DF2\u5B58\u5728\uFF09").action(modelDownloadCommand);
|
|
3334
|
+
program.command("memory:reindex").description("\u8BB0\u5FC6 FTS \u671F\u671B\u6001\u63A2\u6D4B/\u91CD\u5EFA\uFF08\u81EA\u52A9\u4FEE\u590D\u5165\u53E3\uFF1A\u7F3A\u5BF9\u8C61\u5219\u8865\u9F50\uFF0C\u53CC\u540E\u7AEF postgres/sqlite\uFF09").option("--probe", "\u53EA\u8BFB\u63A2\u6D4B\u7F3A\u5931\u5BF9\u8C61\uFF08\u9ED8\u8BA4\u884C\u4E3A\uFF09").option("--apply", "\u5E94\u7528\u7F3A\u5931\u5BF9\u8C61\u5E76\u590D\u63A2\uFF08\u4ECD\u6709\u7F3A\u5931\u5219\u9000\u51FA\u7801 1\uFF09").option("--backend <type>", "\u540E\u7AEF: postgres | sqlite\uFF08\u7F3A\u7701\u6309\u9879\u76EE Prisma datasource \u81EA\u52A8\u5224\u5B9A\uFF09").option("--json", "\u4EE5 JSON \u8F93\u51FA\u62A5\u544A\uFF08\u4F9B\u81EA\u52A8\u5316\u6D88\u8D39\uFF09").action(async (options) => {
|
|
3335
|
+
const backend = options.backend === "postgres" || options.backend === "sqlite" ? options.backend : void 0;
|
|
3336
|
+
if (options.backend && !backend) {
|
|
3337
|
+
console.error(`\u672A\u77E5 backend: ${options.backend}\uFF08\u53EF\u9009 postgres | sqlite\uFF09`);
|
|
3338
|
+
process.exitCode = 2;
|
|
3339
|
+
return;
|
|
3340
|
+
}
|
|
3341
|
+
process.exitCode = await memoryReindexCommand({ ...options, backend });
|
|
3342
|
+
});
|
|
2965
3343
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "add-coder",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.38",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "[codein2027](https://github.com/xiaomingming92/codein2027) - A complete scaffolding for building the ADD programming paradigm, the implementation layer for AI code governance. Core principle: Audit as Infrastructure. Breaks the black-box programming process and cross-session amnesia, evolving programming paradigms into an auditable, traceable, and convergent new era. npx-ready.\n\n[codein2027](https://github.com/xiaomingming92/codein2027) 快速构建 ADD 编程范式的完整脚手架——AI 代码治理的落地方案。以「审计即基础设施」为核心,彻底打破编程过程黑盒与跨轮失忆,让编程范式进化为可审计、可追溯、可收敛的新时代。npx 即用,人人可体验。",
|
|
6
6
|
"repository": {
|
|
@@ -112,9 +112,13 @@
|
|
|
112
112
|
"lint": "eslint src/",
|
|
113
113
|
"lint:fix": "eslint src/ --fix",
|
|
114
114
|
"generate": "tsx src/caijuehub/transcribe/index.ts",
|
|
115
|
+
"contributors": "tsx scripts/gen-contributors.ts --write",
|
|
116
|
+
"contributors:check": "tsx scripts/gen-contributors.ts --check",
|
|
117
|
+
"contributors:audit": "tsx scripts/gen-contributors.ts --check --audit",
|
|
115
118
|
"podman-add-coder-up": "podman-compose --env-file .env.development -f podman-compose.add.yml up -d",
|
|
116
119
|
"podman-add-coder-down": "podman-compose --env-file .env.development -f podman-compose.add.yml down",
|
|
117
120
|
"test": "vitest run",
|
|
118
|
-
"generate:check": "tsx scripts/generate-idempotency-check.ts"
|
|
121
|
+
"generate:check": "tsx scripts/generate-idempotency-check.ts",
|
|
122
|
+
"release:preflight": "tsx scripts/release-preflight.ts"
|
|
119
123
|
}
|
|
120
124
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_version": "0.3.
|
|
2
|
+
"_version": "0.3.38",
|
|
3
3
|
"adapters/claude/hooks/doc-format-guard.mjs": "b21350a0",
|
|
4
4
|
"adapters/claude/hooks/doc-format-guard.ts": "2abf8013",
|
|
5
5
|
"adapters/claude/hooks/lib/claude-env.ts": "170b40ca",
|
|
@@ -229,7 +229,7 @@
|
|
|
229
229
|
"core/rules/profiles/webapp-profile.md": "e493ae7b",
|
|
230
230
|
"core/rules/project_rules.md": "094821ad",
|
|
231
231
|
"core/rules/theory-practice-map.toml": "b1db99fd",
|
|
232
|
-
"core/scripts/db-ensure.sh": "
|
|
232
|
+
"core/scripts/db-ensure.sh": "d46a43c1",
|
|
233
233
|
"core/scripts/gen-plan-index.sh": "47836bec",
|
|
234
234
|
"core/scripts/mcp-server/elicitation/confirm.ts": "924857b8",
|
|
235
235
|
"core/scripts/mcp-server/elicitation/index.ts": "44c5dc3f",
|
|
@@ -280,7 +280,7 @@
|
|
|
280
280
|
"core/scripts/mcp-server/shared/memory/metrics/stage-words.ts": "a81db64b",
|
|
281
281
|
"core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts": "ea83c131",
|
|
282
282
|
"core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts": "1275eef4",
|
|
283
|
-
"core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql": "
|
|
283
|
+
"core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql": "1a439c1a",
|
|
284
284
|
"core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts": "09a6cf59",
|
|
285
285
|
"core/scripts/mcp-server/shared/memory/retrieval/fusion.ts": "c3aa6a4f",
|
|
286
286
|
"core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts": "dbe4a472",
|
|
@@ -323,8 +323,8 @@
|
|
|
323
323
|
"core/scripts/mcp-server/tools/gateway/check_add_route_status.ts": "4caf552c",
|
|
324
324
|
"core/scripts/mcp-server/tools/gateway/check_doc_similarity.ts": "2cba51c5",
|
|
325
325
|
"core/scripts/mcp-server/tools/gateway/check_dps.ts": "f52ef32c",
|
|
326
|
-
"core/scripts/mcp-server/tools/gateway/check_rahs.ts": "
|
|
327
|
-
"core/scripts/mcp-server/tools/gateway/check_spec_sync.ts": "
|
|
326
|
+
"core/scripts/mcp-server/tools/gateway/check_rahs.ts": "a786744d",
|
|
327
|
+
"core/scripts/mcp-server/tools/gateway/check_spec_sync.ts": "adad0b31",
|
|
328
328
|
"core/scripts/mcp-server/tools/gateway/helpers.ts": "f8794a92",
|
|
329
329
|
"core/scripts/mcp-server/tools/gateway/index.ts": "cffe9783",
|
|
330
330
|
"core/scripts/mcp-server/tools/gateway.backup": "65eae393",
|
|
@@ -37,8 +37,24 @@ EOF
|
|
|
37
37
|
echo ">>> 已创建 .env.development"
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
|
-
# ── SQLite
|
|
41
|
-
|
|
40
|
+
# ── SQLite:无需容器;但 FTS5 原生层不在 Prisma schema 内,必须显式应用期望态 ──
|
|
41
|
+
# 背景(Plan add-coder-sqlite-memory-flow §3.2):`prisma db push` 只建 Prisma 表,
|
|
42
|
+
# `add_memory_fts` 虚表 + 3 个同步触发器是原生 DDL → 此前 sqlite 项目 recall_memory 直接
|
|
43
|
+
# `no such table`。此处与 init.ts(src/lib/memory-expected-state.ts)消费**同一份** SQL 真源。
|
|
44
|
+
# 失败语义:仅告警不阻断(exit 0 语义不变)——记忆检索是能力项,不该让环境准备硬失败。
|
|
45
|
+
if [ "$ENGINE" = "sqlite" ]; then
|
|
46
|
+
MAGIC_DIR="${MAGIC_DIR:-.codex}"
|
|
47
|
+
FTS_SQL="$MAGIC_DIR/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql"
|
|
48
|
+
if [ -f "$PROJECT_DIR/$FTS_SQL" ]; then
|
|
49
|
+
echo ">>> 应用记忆 FTS5 期望态($FTS_SQL)..."
|
|
50
|
+
# Prisma 7:db execute 的 datasource 由项目 prisma.config.ts 提供,`--schema` 已移除(带=unknown option)
|
|
51
|
+
(cd "$PROJECT_DIR" && npm exec prisma -- db execute --file "$PROJECT_DIR/$FTS_SQL") \
|
|
52
|
+
|| echo "!!! 记忆 FTS5 期望态应用失败(不阻断):recall_memory 将按 fts-only 降级;修复入口 add-coder memory:reindex --apply"
|
|
53
|
+
else
|
|
54
|
+
echo ">>> 未找到 $FTS_SQL(跳过,不阻断)——请先执行 add-coder sync"
|
|
55
|
+
fi
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
42
58
|
|
|
43
59
|
# ── 自行管理 PostgreSQL ──
|
|
44
60
|
if [ "$CONTAINER" = "none" ] || [ "$CONTAINER" = "manual" ]; then
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
-- 本文件由 scripts/memory/gen-sqlite-fts-sql.ts 生成(真源:src/lib/memory-fts-objects.ts 的 SQLITE_FTS_OBJECTS)
|
|
2
|
+
-- 请勿手工编辑;新增/修改 DDL 请改真源后重跑生成脚本。
|
|
3
|
+
--
|
|
1
4
|
-- Agent Memory FTS 原生层(SQLite 后端)
|
|
2
5
|
-- Plan §8.3 + §17-3 定案:FTS5 trigram 分词器(CJK 友好,≥3 字符 n-gram 匹配;
|
|
3
6
|
-- 短于 3 字符的查询由 adapter 层回退 LIKE —— 见 retrieval/fts/sqlite.ts)
|
|
@@ -6,24 +9,10 @@
|
|
|
6
9
|
-- (其要求 INTEGER rowid),故采用独立 FTS 表 + 触发器同步。
|
|
7
10
|
-- 幂等:全部 IF NOT EXISTS,可重复应用。
|
|
8
11
|
|
|
9
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS add_memory_fts USING fts5(
|
|
10
|
-
memory_id UNINDEXED,
|
|
11
|
-
topic,
|
|
12
|
-
content,
|
|
13
|
-
tokenize = 'trigram'
|
|
14
|
-
);
|
|
12
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS add_memory_fts USING fts5(memory_id UNINDEXED, topic, content, tokenize = 'trigram');
|
|
15
13
|
|
|
16
|
-
CREATE TRIGGER IF NOT EXISTS add_memory_fts_ai AFTER INSERT ON "AddMemory" BEGIN
|
|
17
|
-
INSERT INTO add_memory_fts(memory_id, topic, content)
|
|
18
|
-
VALUES (new.id, new.topic, new.content);
|
|
19
|
-
END;
|
|
14
|
+
CREATE TRIGGER IF NOT EXISTS add_memory_fts_ai AFTER INSERT ON "AddMemory" BEGIN INSERT INTO add_memory_fts(memory_id, topic, content) VALUES (new.id, new.topic, new.content); END;
|
|
20
15
|
|
|
21
|
-
CREATE TRIGGER IF NOT EXISTS add_memory_fts_au AFTER UPDATE ON "AddMemory" BEGIN
|
|
22
|
-
DELETE FROM add_memory_fts WHERE memory_id = old.id;
|
|
23
|
-
INSERT INTO add_memory_fts(memory_id, topic, content)
|
|
24
|
-
VALUES (new.id, new.topic, new.content);
|
|
25
|
-
END;
|
|
16
|
+
CREATE TRIGGER IF NOT EXISTS add_memory_fts_au AFTER UPDATE ON "AddMemory" BEGIN DELETE FROM add_memory_fts WHERE memory_id = old.id; INSERT INTO add_memory_fts(memory_id, topic, content) VALUES (new.id, new.topic, new.content); END;
|
|
26
17
|
|
|
27
|
-
CREATE TRIGGER IF NOT EXISTS add_memory_fts_ad AFTER DELETE ON "AddMemory" BEGIN
|
|
28
|
-
DELETE FROM add_memory_fts WHERE memory_id = old.id;
|
|
29
|
-
END;
|
|
18
|
+
CREATE TRIGGER IF NOT EXISTS add_memory_fts_ad AFTER DELETE ON "AddMemory" BEGIN DELETE FROM add_memory_fts WHERE memory_id = old.id; END;
|
|
@@ -154,7 +154,8 @@ export function registerCheckRahs(server: ToolRegistrar) {
|
|
|
154
154
|
arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json|yml|yaml)`/g) ||
|
|
155
155
|
[]
|
|
156
156
|
).map((f: string) => f.replace(/`/g, "").toLowerCase());
|
|
157
|
-
|
|
157
|
+
// core.quotepath=false:避免非 ASCII 路径被八进制转义后与附录清单比不中(与 check_spec_sync 同修)
|
|
158
|
+
const diff = runCommand("git", ["-c", "core.quotepath=false", "diff", "--name-only"], {
|
|
158
159
|
cwd: PROJECT_ROOT,
|
|
159
160
|
timeout: 5000,
|
|
160
161
|
});
|
|
@@ -77,14 +77,18 @@ export function registerCheckSpecSync(server: ToolRegistrar) {
|
|
|
77
77
|
lines.push(`add-route: ${arFile}`);
|
|
78
78
|
const arContent = (await readFileSafe(join(plansDir, arFile))) || "";
|
|
79
79
|
// 提取 add-route 附录文件清单(toml 纳入:sync-magic-rules.toml 等控制面文件,2026-08-18 修复)
|
|
80
|
-
|
|
80
|
+
// sql/prisma 纳入(2026-09-16 修复):原生 DDL 与 schema 是常见交付物,此前不在白名单 →
|
|
81
|
+
// 附录里明明登记了 `.../sqlite-fts5.sql` 仍被判"不在附录中"(假告警)。
|
|
82
|
+
const appendixFiles = (arContent.match(/`[^`]+\.(ts|tsx|js|jsx|sh|sql|prisma|md|json|yml|yaml|toml)`/g) || [])
|
|
81
83
|
.map((f: string) => f.replace(/`/g, ""));
|
|
82
84
|
lines.push(`附录文件: ${appendixFiles.length} 个`);
|
|
83
85
|
|
|
84
86
|
// git diff 变更文件(win32 下 git 为 .cmd → runCommand 自动解析,issue #10 跨端修复)
|
|
85
87
|
let diffFiles: string[] = [];
|
|
86
88
|
try {
|
|
87
|
-
|
|
89
|
+
// core.quotepath=false:默认 git 会把非 ASCII 路径转成 "\346\236..." 八进制转义 →
|
|
90
|
+
// 与附录里的真实中文路径永远比不中(2026-09-16 修复:架构文档类路径的假告警)。
|
|
91
|
+
const diff = runCommand("git", ["-c", "core.quotepath=false", "diff", "--name-only"], {
|
|
88
92
|
cwd: PROJECT_ROOT,
|
|
89
93
|
timeout: 5000,
|
|
90
94
|
});
|