flavor-code 1.2.13 → 1.2.14

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 (131) hide show
  1. package/README.md +3 -0
  2. package/README.zh-CN.md +3 -0
  3. package/dist/{app-Z3YWFIBB.js → app-UJAGMKD5.js} +2 -2
  4. package/dist/astgraph/db.mjs +237 -237
  5. package/dist/astgraph/extract.mjs +239 -239
  6. package/dist/astgraph/flavor-plugin.json +25 -25
  7. package/dist/astgraph/grammars.mjs +68 -68
  8. package/dist/astgraph/index.js +301 -301
  9. package/dist/astgraph/indexer.mjs +175 -175
  10. package/dist/astgraph/query.mjs +165 -165
  11. package/dist/astgraph/resolve.mjs +93 -93
  12. package/dist/astgraph/vendor/tree-sitter.js +3980 -3980
  13. package/dist/astgraph/vendor/zod/LICENSE +21 -21
  14. package/dist/astgraph/vendor/zod/index.js +4 -4
  15. package/dist/astgraph/vendor/zod/locales/index.js +1 -1
  16. package/dist/astgraph/vendor/zod/locales/package.json +7 -7
  17. package/dist/astgraph/vendor/zod/package.json +135 -135
  18. package/dist/astgraph/vendor/zod/v4/classic/checks.js +1 -1
  19. package/dist/astgraph/vendor/zod/v4/classic/coerce.js +17 -17
  20. package/dist/astgraph/vendor/zod/v4/classic/compat.js +31 -31
  21. package/dist/astgraph/vendor/zod/v4/classic/errors.js +48 -48
  22. package/dist/astgraph/vendor/zod/v4/classic/external.js +20 -20
  23. package/dist/astgraph/vendor/zod/v4/classic/from-json-schema.js +599 -599
  24. package/dist/astgraph/vendor/zod/v4/classic/index.js +4 -4
  25. package/dist/astgraph/vendor/zod/v4/classic/iso.js +30 -30
  26. package/dist/astgraph/vendor/zod/v4/classic/package.json +7 -7
  27. package/dist/astgraph/vendor/zod/v4/classic/parse.js +15 -15
  28. package/dist/astgraph/vendor/zod/v4/classic/schemas.js +1395 -1395
  29. package/dist/astgraph/vendor/zod/v4/core/api.js +1087 -1087
  30. package/dist/astgraph/vendor/zod/v4/core/checks.js +575 -575
  31. package/dist/astgraph/vendor/zod/v4/core/core.js +78 -78
  32. package/dist/astgraph/vendor/zod/v4/core/doc.js +35 -35
  33. package/dist/astgraph/vendor/zod/v4/core/errors.js +185 -185
  34. package/dist/astgraph/vendor/zod/v4/core/index.js +16 -16
  35. package/dist/astgraph/vendor/zod/v4/core/json-schema-generator.js +95 -95
  36. package/dist/astgraph/vendor/zod/v4/core/json-schema-processors.js +601 -601
  37. package/dist/astgraph/vendor/zod/v4/core/json-schema.js +1 -1
  38. package/dist/astgraph/vendor/zod/v4/core/package.json +7 -7
  39. package/dist/astgraph/vendor/zod/v4/core/parse.js +93 -93
  40. package/dist/astgraph/vendor/zod/v4/core/regexes.js +139 -139
  41. package/dist/astgraph/vendor/zod/v4/core/registries.js +51 -51
  42. package/dist/astgraph/vendor/zod/v4/core/schemas.js +2239 -2239
  43. package/dist/astgraph/vendor/zod/v4/core/standard-schema.js +1 -1
  44. package/dist/astgraph/vendor/zod/v4/core/to-json-schema.js +448 -448
  45. package/dist/astgraph/vendor/zod/v4/core/util.js +674 -674
  46. package/dist/astgraph/vendor/zod/v4/core/versions.js +5 -5
  47. package/dist/astgraph/vendor/zod/v4/index.js +3 -3
  48. package/dist/astgraph/vendor/zod/v4/locales/ar.js +106 -106
  49. package/dist/astgraph/vendor/zod/v4/locales/az.js +105 -105
  50. package/dist/astgraph/vendor/zod/v4/locales/be.js +156 -156
  51. package/dist/astgraph/vendor/zod/v4/locales/bg.js +120 -120
  52. package/dist/astgraph/vendor/zod/v4/locales/ca.js +107 -107
  53. package/dist/astgraph/vendor/zod/v4/locales/cs.js +111 -111
  54. package/dist/astgraph/vendor/zod/v4/locales/da.js +115 -115
  55. package/dist/astgraph/vendor/zod/v4/locales/de.js +108 -108
  56. package/dist/astgraph/vendor/zod/v4/locales/el.js +109 -109
  57. package/dist/astgraph/vendor/zod/v4/locales/en.js +113 -113
  58. package/dist/astgraph/vendor/zod/v4/locales/eo.js +109 -109
  59. package/dist/astgraph/vendor/zod/v4/locales/es.js +132 -132
  60. package/dist/astgraph/vendor/zod/v4/locales/fa.js +114 -114
  61. package/dist/astgraph/vendor/zod/v4/locales/fi.js +112 -112
  62. package/dist/astgraph/vendor/zod/v4/locales/fr-CA.js +107 -107
  63. package/dist/astgraph/vendor/zod/v4/locales/fr.js +125 -125
  64. package/dist/astgraph/vendor/zod/v4/locales/he.js +214 -214
  65. package/dist/astgraph/vendor/zod/v4/locales/hr.js +122 -122
  66. package/dist/astgraph/vendor/zod/v4/locales/hu.js +108 -108
  67. package/dist/astgraph/vendor/zod/v4/locales/hy.js +147 -147
  68. package/dist/astgraph/vendor/zod/v4/locales/id.js +106 -106
  69. package/dist/astgraph/vendor/zod/v4/locales/index.js +52 -52
  70. package/dist/astgraph/vendor/zod/v4/locales/is.js +109 -109
  71. package/dist/astgraph/vendor/zod/v4/locales/it.js +108 -108
  72. package/dist/astgraph/vendor/zod/v4/locales/ja.js +107 -107
  73. package/dist/astgraph/vendor/zod/v4/locales/ka.js +112 -112
  74. package/dist/astgraph/vendor/zod/v4/locales/kh.js +5 -5
  75. package/dist/astgraph/vendor/zod/v4/locales/km.js +110 -110
  76. package/dist/astgraph/vendor/zod/v4/locales/ko.js +111 -111
  77. package/dist/astgraph/vendor/zod/v4/locales/lt.js +203 -203
  78. package/dist/astgraph/vendor/zod/v4/locales/mk.js +109 -109
  79. package/dist/astgraph/vendor/zod/v4/locales/ms.js +107 -107
  80. package/dist/astgraph/vendor/zod/v4/locales/nl.js +110 -110
  81. package/dist/astgraph/vendor/zod/v4/locales/no.js +108 -108
  82. package/dist/astgraph/vendor/zod/v4/locales/ota.js +109 -109
  83. package/dist/astgraph/vendor/zod/v4/locales/package.json +7 -7
  84. package/dist/astgraph/vendor/zod/v4/locales/pl.js +109 -109
  85. package/dist/astgraph/vendor/zod/v4/locales/ps.js +114 -114
  86. package/dist/astgraph/vendor/zod/v4/locales/pt.js +108 -108
  87. package/dist/astgraph/vendor/zod/v4/locales/ro.js +119 -119
  88. package/dist/astgraph/vendor/zod/v4/locales/ru.js +156 -156
  89. package/dist/astgraph/vendor/zod/v4/locales/sl.js +109 -109
  90. package/dist/astgraph/vendor/zod/v4/locales/sv.js +110 -110
  91. package/dist/astgraph/vendor/zod/v4/locales/ta.js +110 -110
  92. package/dist/astgraph/vendor/zod/v4/locales/th.js +110 -110
  93. package/dist/astgraph/vendor/zod/v4/locales/tr.js +105 -105
  94. package/dist/astgraph/vendor/zod/v4/locales/ua.js +5 -5
  95. package/dist/astgraph/vendor/zod/v4/locales/uk.js +108 -108
  96. package/dist/astgraph/vendor/zod/v4/locales/ur.js +110 -110
  97. package/dist/astgraph/vendor/zod/v4/locales/uz.js +110 -110
  98. package/dist/astgraph/vendor/zod/v4/locales/vi.js +108 -108
  99. package/dist/astgraph/vendor/zod/v4/locales/yo.js +107 -107
  100. package/dist/astgraph/vendor/zod/v4/locales/zh-CN.js +109 -109
  101. package/dist/astgraph/vendor/zod/v4/locales/zh-TW.js +107 -107
  102. package/dist/astgraph/vendor/zod/v4/mini/checks.js +1 -1
  103. package/dist/astgraph/vendor/zod/v4/mini/coerce.js +22 -22
  104. package/dist/astgraph/vendor/zod/v4/mini/external.js +14 -14
  105. package/dist/astgraph/vendor/zod/v4/mini/index.js +3 -3
  106. package/dist/astgraph/vendor/zod/v4/mini/iso.js +34 -34
  107. package/dist/astgraph/vendor/zod/v4/mini/package.json +7 -7
  108. package/dist/astgraph/vendor/zod/v4/mini/parse.js +1 -1
  109. package/dist/astgraph/vendor/zod/v4/mini/schemas.js +961 -961
  110. package/dist/astgraph/vendor/zod/v4/package.json +7 -7
  111. package/dist/{chunk-Y5MZDG7I.js → chunk-4YXDGGIC.js} +4611 -3754
  112. package/dist/{chunk-XFCJXRJ2.js → chunk-IG4Z23CL.js} +6 -0
  113. package/dist/{chunk-G6Q2Y2A7.js → chunk-KL6TR2KG.js} +1 -1
  114. package/dist/cli.js +5 -5
  115. package/dist/config/schema.d.ts +6 -0
  116. package/dist/desktop/main.js +4900 -4038
  117. package/dist/desktop-renderer/assets/{index-CCnSpff0.js → index-CcD0D6x0.js} +1 -1
  118. package/dist/desktop-renderer/index.html +1 -1
  119. package/dist/evolve/loader.d.ts +21 -0
  120. package/dist/evolve/service.d.ts +37 -0
  121. package/dist/evolve/store.d.ts +91 -0
  122. package/dist/hooks/types.d.ts +5 -1
  123. package/dist/{load-6ZMEBHSG.js → load-GKA7P6RJ.js} +1 -1
  124. package/dist/plugins/host.d.ts +9 -0
  125. package/dist/plugins/types.d.ts +1 -0
  126. package/dist/sdk/index.js +3 -3
  127. package/dist/tools/shell.d.ts +13 -0
  128. package/dist/ui/commands.d.ts +5 -2
  129. package/dist/ui/session.d.ts +1 -0
  130. package/package.json +1 -1
  131. package//346/212/200/346/234/257/346/226/271/346/241/210/346/212/245/345/221/212.md +303 -1
package/README.md CHANGED
@@ -37,6 +37,7 @@ Flavor Code connects to OpenAI, Anthropic, or compatible services and works with
37
37
  | 🧠 | **Local long-term context** | Memory, Skills, plugins, and project guides stored on your machine |
38
38
  | 🔎 | **Code graph navigation** | A local AST code-graph index (`.flavor/astgraph/`) powers `ast_search`/`ast_callers`/`ast_impact` queries for precise symbol lookup and reachability tracing |
39
39
  | 🎨 | **E2E requirement-to-delivery** | From a rough requirement or a design export to a delivered product: PRD, interactive prototype, visual implementation, API integration, autonomous acceptance, and scored delivery (Electron only) |
40
+ | 🔁 | **Bounded self-improvement** | Repeated tool failures are captured, deduped, and proposed as suggestions; fixes ship as plugins that must pass sandbox verification and the test suite before hot-reload (`/evolve`) |
40
41
  | 🛡️ | **Clear permission boundaries** | Independent control over read, write, Shell, network, and destructive actions; Docker supported |
41
42
 
42
43
  ## Quick Start
@@ -154,6 +155,7 @@ Common commands:
154
155
  | `/mcp` | View and manage MCP servers |
155
156
  | `/loop <goal>` | Run an autonomous loop with verification |
156
157
  | `/goal <objective>` | Run the plan, execute, adversarial-review workflow |
158
+ | `/evolve <signals\|suggest\|improve ...>` | Self-improvement loop: review repeated tool failures, scaffold fix plugins, verify and hot-reload them |
157
159
  | `/pals`, `/chat`, `/co-work` | Discover and collaborate with other local CLI instances |
158
160
  | `/audit` | View tool failure audits |
159
161
 
@@ -261,6 +263,7 @@ Project runtime data lives under `.flavor/`:
261
263
  ├── memory/ # Long-term memory
262
264
  ├── traces/ # Optional execution traces
263
265
  ├── audit.jsonl # Tool failure audits
266
+ ├── evolve/ # Self-improvement signals and run reflections
264
267
  ├── skills/ # Project skills
265
268
  └── plugins/ # Project plugins
266
269
  ```
package/README.zh-CN.md CHANGED
@@ -37,6 +37,7 @@ Flavor Code 接入 OpenAI、Anthropic 或兼容服务,在受控工作区内使
37
37
  | 🧠 | **本地长期上下文** | 记忆、Skill、插件和项目指南均保存在本机 |
38
38
  | 🔎 | **代码图导航** | 本地 AST 代码图索引(`.flavor/astgraph/`),通过 `ast_search`/`ast_callers`/`ast_impact` 等查询精确定位符号、追踪可达性 |
39
39
  | 🎨 | **E2E 需求到交付** | 从粗需求或设计稿到可交付产品:PRD、交互原型、视觉还原、接口联调、自主验收与评分交付(仅 Electron) |
40
+ | 🔁 | **有界自进化** | 重复的工具失败被捕获、去重并形成建议;修复以插件形式落地,必须通过沙箱验证与测试套件后才能热重载(`/evolve`) |
40
41
  | 🛡️ | **明确的权限边界** | 分别控制读、写、Shell、网络和破坏性操作,也可使用 Docker |
41
42
 
42
43
  ## 快速开始
@@ -154,6 +155,7 @@ OAuth PKCE 的运行时行为与配置约定见 [PKCE 规范](./docs/specs/pkce-
154
155
  | `/mcp` | 查看和管理 MCP 服务 |
155
156
  | `/loop <goal>` | 运行带验证的自治循环 |
156
157
  | `/goal <objective>` | 运行规划、执行、对抗审查流程 |
158
+ | `/evolve <signals\|suggest\|improve ...>` | 自进化循环:查看重复工具失败、脚手架修复插件、验证并热重载 |
157
159
  | `/pals`、`/chat`、`/co-work` | 发现并协作其他本机 CLI 实例 |
158
160
  | `/audit` | 查看工具失败审计 |
159
161
 
@@ -261,6 +263,7 @@ Skill 是带有 YAML 头信息的 `SKILL.md`,放在 `.flavor/skills/<name>/`
261
263
  ├── memory/ # 长期记忆
262
264
  ├── traces/ # 可选执行 trace
263
265
  ├── audit.jsonl # 工具失败审计
266
+ ├── evolve/ # 自进化信号与运行评估记录
264
267
  ├── skills/ # 项目 Skill
265
268
  └── plugins/ # 项目插件
266
269
  ```
@@ -15,8 +15,8 @@ import {
15
15
  packageVersion,
16
16
  redactErrorText,
17
17
  transcriptReducer
18
- } from "./chunk-Y5MZDG7I.js";
19
- import "./chunk-XFCJXRJ2.js";
18
+ } from "./chunk-4YXDGGIC.js";
19
+ import "./chunk-IG4Z23CL.js";
20
20
  import {
21
21
  Box_default,
22
22
  ClockContext,
@@ -1,237 +1,237 @@
1
- // astgraph graph database — SQLite-backed node/edge graph (codegraph-inspired).
2
- // Uses node:sqlite with WAL mode so concurrent readers never block on a writer.
3
-
4
- import { mkdirSync } from "node:fs";
5
- import { dirname } from "node:path";
6
- import { DatabaseSync } from "node:sqlite";
7
-
8
- const SCHEMA = `
9
- CREATE TABLE IF NOT EXISTS schema_versions (
10
- version INTEGER PRIMARY KEY,
11
- applied_at INTEGER NOT NULL,
12
- description TEXT
13
- );
14
-
15
- CREATE TABLE IF NOT EXISTS files (
16
- path TEXT PRIMARY KEY,
17
- content_hash TEXT NOT NULL,
18
- language TEXT NOT NULL,
19
- size INTEGER NOT NULL,
20
- modified_at INTEGER NOT NULL DEFAULT 0,
21
- indexed_at INTEGER NOT NULL DEFAULT 0,
22
- node_count INTEGER NOT NULL DEFAULT 0
23
- );
24
-
25
- CREATE TABLE IF NOT EXISTS nodes (
26
- id TEXT PRIMARY KEY,
27
- kind TEXT NOT NULL,
28
- name TEXT NOT NULL,
29
- qualified_name TEXT NOT NULL,
30
- file_path TEXT NOT NULL,
31
- language TEXT NOT NULL,
32
- start_line INTEGER NOT NULL,
33
- end_line INTEGER NOT NULL,
34
- signature TEXT,
35
- docstring TEXT,
36
- is_exported INTEGER NOT NULL DEFAULT 0,
37
- is_async INTEGER NOT NULL DEFAULT 0
38
- );
39
-
40
- CREATE TABLE IF NOT EXISTS edges (
41
- id INTEGER PRIMARY KEY AUTOINCREMENT,
42
- source TEXT NOT NULL,
43
- target TEXT NOT NULL,
44
- kind TEXT NOT NULL,
45
- line INTEGER,
46
- col INTEGER,
47
- FOREIGN KEY (source) REFERENCES nodes(id) ON DELETE CASCADE,
48
- FOREIGN KEY (target) REFERENCES nodes(id) ON DELETE CASCADE
49
- );
50
-
51
- CREATE TABLE IF NOT EXISTS unresolved_refs (
52
- id INTEGER PRIMARY KEY AUTOINCREMENT,
53
- from_node_id TEXT,
54
- reference_name TEXT NOT NULL,
55
- reference_kind TEXT NOT NULL,
56
- line INTEGER NOT NULL,
57
- col INTEGER NOT NULL,
58
- file_path TEXT NOT NULL DEFAULT '',
59
- module_specifier TEXT,
60
- status TEXT NOT NULL DEFAULT 'pending',
61
- FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE
62
- );
63
-
64
- CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
65
- CREATE INDEX IF NOT EXISTS idx_nodes_lower_name ON nodes(lower(name));
66
- CREATE INDEX IF NOT EXISTS idx_nodes_file_path ON nodes(file_path);
67
- CREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind);
68
- CREATE INDEX IF NOT EXISTS idx_edges_source_kind ON edges(source, kind);
69
- CREATE INDEX IF NOT EXISTS idx_edges_target_kind ON edges(target, kind);
70
- CREATE UNIQUE INDEX IF NOT EXISTS idx_edges_identity
71
- ON edges(source, target, kind, IFNULL(line, -1), IFNULL(col, -1));
72
- CREATE INDEX IF NOT EXISTS idx_unresolved_from ON unresolved_refs(from_node_id);
73
- CREATE INDEX IF NOT EXISTS idx_unresolved_name ON unresolved_refs(reference_name);
74
-
75
- CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
76
- name,
77
- qualified_name,
78
- signature,
79
- docstring,
80
- content='nodes',
81
- content_rowid='rowid'
82
- );
83
-
84
- CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes BEGIN
85
- INSERT INTO nodes_fts(rowid, name, qualified_name, signature, docstring)
86
- VALUES (NEW.rowid, NEW.name, NEW.qualified_name, NEW.signature, NEW.docstring);
87
- END;
88
-
89
- CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes BEGIN
90
- INSERT INTO nodes_fts(nodes_fts, rowid, name, qualified_name, signature, docstring)
91
- VALUES ('delete', OLD.rowid, OLD.name, OLD.qualified_name, OLD.signature, OLD.docstring);
92
- END;
93
-
94
- CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes BEGIN
95
- INSERT INTO nodes_fts(nodes_fts, rowid, name, qualified_name, signature, docstring)
96
- VALUES ('delete', OLD.rowid, OLD.name, OLD.qualified_name, OLD.signature, OLD.docstring);
97
- INSERT INTO nodes_fts(rowid, name, qualified_name, signature, docstring)
98
- VALUES (NEW.rowid, NEW.name, NEW.qualified_name, NEW.signature, NEW.docstring);
99
- END;
100
-
101
- CREATE TABLE IF NOT EXISTS project_metadata (
102
- key TEXT PRIMARY KEY,
103
- value TEXT NOT NULL,
104
- updated_at INTEGER NOT NULL
105
- );
106
- `;
107
-
108
- /** Open (creating when absent) the graph database at `path`. Returns a DatabaseSync. */
109
- export function openDb(path) {
110
- if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
111
- const db = new DatabaseSync(path);
112
- db.exec("PRAGMA journal_mode = WAL");
113
- db.exec("PRAGMA foreign_keys = ON");
114
- db.exec("PRAGMA synchronous = NORMAL");
115
- db.exec(SCHEMA);
116
- const version = db.prepare("SELECT COUNT(*) AS count FROM schema_versions").get();
117
- if (version.count === 0) {
118
- db.prepare("INSERT INTO schema_versions(version, applied_at, description) VALUES (?, ?, ?)")
119
- .run(1, Date.now(), "Initial astgraph schema");
120
- }
121
- return db;
122
- }
123
-
124
- /** Insert or replace the tracking record for one source file. */
125
- export function upsertFileRecord(db, record) {
126
- db.prepare(`
127
- INSERT INTO files(path, content_hash, language, size, modified_at, indexed_at, node_count)
128
- VALUES (?, ?, ?, ?, ?, ?, ?)
129
- ON CONFLICT(path) DO UPDATE SET
130
- content_hash = excluded.content_hash,
131
- language = excluded.language,
132
- size = excluded.size,
133
- modified_at = excluded.modified_at,
134
- indexed_at = excluded.indexed_at,
135
- node_count = excluded.node_count
136
- `).run(
137
- record.path, record.contentHash, record.language, record.size,
138
- record.modifiedAt ?? Date.now(), record.indexedAt ?? Date.now(), record.nodeCount ?? 0,
139
- );
140
- }
141
-
142
- /** Read the tracking record for one source file, or undefined when absent. */
143
- export function getFileRecord(db, path) {
144
- const row = db.prepare("SELECT * FROM files WHERE path = ?").get(path);
145
- if (row === undefined) return undefined;
146
- return {
147
- path: row.path, contentHash: row.content_hash, language: row.language,
148
- size: row.size, modifiedAt: row.modified_at, indexedAt: row.indexed_at,
149
- nodeCount: row.node_count,
150
- };
151
- }
152
-
153
- /** Delete a file record; nodes cascade (and with them edges + refs + FTS rows). */
154
- export function deleteFileRecord(db, path) {
155
- db.prepare("DELETE FROM nodes WHERE file_path = ?").run(path);
156
- db.prepare("DELETE FROM files WHERE path = ?").run(path);
157
- }
158
-
159
- /**
160
- * Replace every node belonging to a file. Runs inside one transaction; the FTS
161
- * triggers keep nodes_fts in sync.
162
- */
163
- export function replaceNodes(db, filePath, nodes) {
164
- const insert = db.prepare(`
165
- INSERT OR REPLACE INTO nodes(
166
- id, kind, name, qualified_name, file_path, language,
167
- start_line, end_line, signature, docstring, is_exported, is_async
168
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
169
- `);
170
- const clear = db.prepare("DELETE FROM nodes WHERE file_path = ?");
171
- db.exec("BEGIN");
172
- try {
173
- clear.run(filePath);
174
- for (const node of nodes) {
175
- insert.run(
176
- node.id, node.kind, node.name, node.qualifiedName, node.filePath, node.language,
177
- node.startLine, node.endLine, node.signature ?? null, node.docstring ?? null,
178
- node.isExported ? 1 : 0, node.isAsync ? 1 : 0,
179
- );
180
- }
181
- db.exec("COMMIT");
182
- } catch (error) {
183
- db.exec("ROLLBACK");
184
- throw error;
185
- }
186
- }
187
-
188
- /** Insert one edge; duplicate (source, target, kind, line, col) rows are ignored. */
189
- export function insertEdge(db, edge) {
190
- db.prepare(`
191
- INSERT OR IGNORE INTO edges(source, target, kind, line, col) VALUES (?, ?, ?, ?, ?)
192
- `).run(edge.source, edge.target, edge.kind, edge.line ?? null, edge.col ?? null);
193
- }
194
-
195
- /** Delete every edge whose source OR target belongs to a file. */
196
- export function deleteEdgesForFile(db, filePath) {
197
- db.prepare(`
198
- DELETE FROM edges WHERE source IN (SELECT id FROM nodes WHERE file_path = ?)
199
- OR target IN (SELECT id FROM nodes WHERE file_path = ?)
200
- `).run(filePath, filePath);
201
- }
202
-
203
- /** Insert one unresolved cross-file reference (status 'pending'). */
204
- export function insertUnresolvedRef(db, ref) {
205
- db.prepare(`
206
- INSERT INTO unresolved_refs(from_node_id, reference_name, reference_kind, line, col, file_path, module_specifier, status)
207
- VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')
208
- `).run(ref.fromNodeId, ref.referenceName, ref.referenceKind, ref.line, ref.col, ref.filePath, ref.moduleSpecifier ?? null);
209
- }
210
-
211
- /** Delete every unresolved reference originating from a file. */
212
- export function deleteUnresolvedRefsForFile(db, filePath) {
213
- db.prepare("DELETE FROM unresolved_refs WHERE file_path = ?").run(filePath);
214
- }
215
-
216
- /** Summary counters used by /ast status. */
217
- export function stats(db) {
218
- const files = db.prepare("SELECT COUNT(*) AS count FROM files").get().count;
219
- const nodes = db.prepare("SELECT COUNT(*) AS count FROM nodes").get().count;
220
- const edges = db.prepare("SELECT COUNT(*) AS count FROM edges").get().count;
221
- const unresolved = db.prepare("SELECT COUNT(*) AS count FROM unresolved_refs WHERE status = 'pending'").get().count;
222
- return { files, nodes, edges, unresolved };
223
- }
224
-
225
- /** Store a provenance metadata value. */
226
- export function setMetadata(db, key, value) {
227
- db.prepare(`
228
- INSERT INTO project_metadata(key, value, updated_at) VALUES (?, ?, ?)
229
- ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
230
- `).run(key, value, Date.now());
231
- }
232
-
233
- /** Read a provenance metadata value, or undefined when absent. */
234
- export function getMetadata(db, key) {
235
- const row = db.prepare("SELECT value FROM project_metadata WHERE key = ?").get(key);
236
- return row?.value;
237
- }
1
+ // astgraph graph database — SQLite-backed node/edge graph (codegraph-inspired).
2
+ // Uses node:sqlite with WAL mode so concurrent readers never block on a writer.
3
+
4
+ import { mkdirSync } from "node:fs";
5
+ import { dirname } from "node:path";
6
+ import { DatabaseSync } from "node:sqlite";
7
+
8
+ const SCHEMA = `
9
+ CREATE TABLE IF NOT EXISTS schema_versions (
10
+ version INTEGER PRIMARY KEY,
11
+ applied_at INTEGER NOT NULL,
12
+ description TEXT
13
+ );
14
+
15
+ CREATE TABLE IF NOT EXISTS files (
16
+ path TEXT PRIMARY KEY,
17
+ content_hash TEXT NOT NULL,
18
+ language TEXT NOT NULL,
19
+ size INTEGER NOT NULL,
20
+ modified_at INTEGER NOT NULL DEFAULT 0,
21
+ indexed_at INTEGER NOT NULL DEFAULT 0,
22
+ node_count INTEGER NOT NULL DEFAULT 0
23
+ );
24
+
25
+ CREATE TABLE IF NOT EXISTS nodes (
26
+ id TEXT PRIMARY KEY,
27
+ kind TEXT NOT NULL,
28
+ name TEXT NOT NULL,
29
+ qualified_name TEXT NOT NULL,
30
+ file_path TEXT NOT NULL,
31
+ language TEXT NOT NULL,
32
+ start_line INTEGER NOT NULL,
33
+ end_line INTEGER NOT NULL,
34
+ signature TEXT,
35
+ docstring TEXT,
36
+ is_exported INTEGER NOT NULL DEFAULT 0,
37
+ is_async INTEGER NOT NULL DEFAULT 0
38
+ );
39
+
40
+ CREATE TABLE IF NOT EXISTS edges (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ source TEXT NOT NULL,
43
+ target TEXT NOT NULL,
44
+ kind TEXT NOT NULL,
45
+ line INTEGER,
46
+ col INTEGER,
47
+ FOREIGN KEY (source) REFERENCES nodes(id) ON DELETE CASCADE,
48
+ FOREIGN KEY (target) REFERENCES nodes(id) ON DELETE CASCADE
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS unresolved_refs (
52
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ from_node_id TEXT,
54
+ reference_name TEXT NOT NULL,
55
+ reference_kind TEXT NOT NULL,
56
+ line INTEGER NOT NULL,
57
+ col INTEGER NOT NULL,
58
+ file_path TEXT NOT NULL DEFAULT '',
59
+ module_specifier TEXT,
60
+ status TEXT NOT NULL DEFAULT 'pending',
61
+ FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE
62
+ );
63
+
64
+ CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
65
+ CREATE INDEX IF NOT EXISTS idx_nodes_lower_name ON nodes(lower(name));
66
+ CREATE INDEX IF NOT EXISTS idx_nodes_file_path ON nodes(file_path);
67
+ CREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind);
68
+ CREATE INDEX IF NOT EXISTS idx_edges_source_kind ON edges(source, kind);
69
+ CREATE INDEX IF NOT EXISTS idx_edges_target_kind ON edges(target, kind);
70
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_edges_identity
71
+ ON edges(source, target, kind, IFNULL(line, -1), IFNULL(col, -1));
72
+ CREATE INDEX IF NOT EXISTS idx_unresolved_from ON unresolved_refs(from_node_id);
73
+ CREATE INDEX IF NOT EXISTS idx_unresolved_name ON unresolved_refs(reference_name);
74
+
75
+ CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
76
+ name,
77
+ qualified_name,
78
+ signature,
79
+ docstring,
80
+ content='nodes',
81
+ content_rowid='rowid'
82
+ );
83
+
84
+ CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes BEGIN
85
+ INSERT INTO nodes_fts(rowid, name, qualified_name, signature, docstring)
86
+ VALUES (NEW.rowid, NEW.name, NEW.qualified_name, NEW.signature, NEW.docstring);
87
+ END;
88
+
89
+ CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes BEGIN
90
+ INSERT INTO nodes_fts(nodes_fts, rowid, name, qualified_name, signature, docstring)
91
+ VALUES ('delete', OLD.rowid, OLD.name, OLD.qualified_name, OLD.signature, OLD.docstring);
92
+ END;
93
+
94
+ CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes BEGIN
95
+ INSERT INTO nodes_fts(nodes_fts, rowid, name, qualified_name, signature, docstring)
96
+ VALUES ('delete', OLD.rowid, OLD.name, OLD.qualified_name, OLD.signature, OLD.docstring);
97
+ INSERT INTO nodes_fts(rowid, name, qualified_name, signature, docstring)
98
+ VALUES (NEW.rowid, NEW.name, NEW.qualified_name, NEW.signature, NEW.docstring);
99
+ END;
100
+
101
+ CREATE TABLE IF NOT EXISTS project_metadata (
102
+ key TEXT PRIMARY KEY,
103
+ value TEXT NOT NULL,
104
+ updated_at INTEGER NOT NULL
105
+ );
106
+ `;
107
+
108
+ /** Open (creating when absent) the graph database at `path`. Returns a DatabaseSync. */
109
+ export function openDb(path) {
110
+ if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
111
+ const db = new DatabaseSync(path);
112
+ db.exec("PRAGMA journal_mode = WAL");
113
+ db.exec("PRAGMA foreign_keys = ON");
114
+ db.exec("PRAGMA synchronous = NORMAL");
115
+ db.exec(SCHEMA);
116
+ const version = db.prepare("SELECT COUNT(*) AS count FROM schema_versions").get();
117
+ if (version.count === 0) {
118
+ db.prepare("INSERT INTO schema_versions(version, applied_at, description) VALUES (?, ?, ?)")
119
+ .run(1, Date.now(), "Initial astgraph schema");
120
+ }
121
+ return db;
122
+ }
123
+
124
+ /** Insert or replace the tracking record for one source file. */
125
+ export function upsertFileRecord(db, record) {
126
+ db.prepare(`
127
+ INSERT INTO files(path, content_hash, language, size, modified_at, indexed_at, node_count)
128
+ VALUES (?, ?, ?, ?, ?, ?, ?)
129
+ ON CONFLICT(path) DO UPDATE SET
130
+ content_hash = excluded.content_hash,
131
+ language = excluded.language,
132
+ size = excluded.size,
133
+ modified_at = excluded.modified_at,
134
+ indexed_at = excluded.indexed_at,
135
+ node_count = excluded.node_count
136
+ `).run(
137
+ record.path, record.contentHash, record.language, record.size,
138
+ record.modifiedAt ?? Date.now(), record.indexedAt ?? Date.now(), record.nodeCount ?? 0,
139
+ );
140
+ }
141
+
142
+ /** Read the tracking record for one source file, or undefined when absent. */
143
+ export function getFileRecord(db, path) {
144
+ const row = db.prepare("SELECT * FROM files WHERE path = ?").get(path);
145
+ if (row === undefined) return undefined;
146
+ return {
147
+ path: row.path, contentHash: row.content_hash, language: row.language,
148
+ size: row.size, modifiedAt: row.modified_at, indexedAt: row.indexed_at,
149
+ nodeCount: row.node_count,
150
+ };
151
+ }
152
+
153
+ /** Delete a file record; nodes cascade (and with them edges + refs + FTS rows). */
154
+ export function deleteFileRecord(db, path) {
155
+ db.prepare("DELETE FROM nodes WHERE file_path = ?").run(path);
156
+ db.prepare("DELETE FROM files WHERE path = ?").run(path);
157
+ }
158
+
159
+ /**
160
+ * Replace every node belonging to a file. Runs inside one transaction; the FTS
161
+ * triggers keep nodes_fts in sync.
162
+ */
163
+ export function replaceNodes(db, filePath, nodes) {
164
+ const insert = db.prepare(`
165
+ INSERT OR REPLACE INTO nodes(
166
+ id, kind, name, qualified_name, file_path, language,
167
+ start_line, end_line, signature, docstring, is_exported, is_async
168
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
169
+ `);
170
+ const clear = db.prepare("DELETE FROM nodes WHERE file_path = ?");
171
+ db.exec("BEGIN");
172
+ try {
173
+ clear.run(filePath);
174
+ for (const node of nodes) {
175
+ insert.run(
176
+ node.id, node.kind, node.name, node.qualifiedName, node.filePath, node.language,
177
+ node.startLine, node.endLine, node.signature ?? null, node.docstring ?? null,
178
+ node.isExported ? 1 : 0, node.isAsync ? 1 : 0,
179
+ );
180
+ }
181
+ db.exec("COMMIT");
182
+ } catch (error) {
183
+ db.exec("ROLLBACK");
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ /** Insert one edge; duplicate (source, target, kind, line, col) rows are ignored. */
189
+ export function insertEdge(db, edge) {
190
+ db.prepare(`
191
+ INSERT OR IGNORE INTO edges(source, target, kind, line, col) VALUES (?, ?, ?, ?, ?)
192
+ `).run(edge.source, edge.target, edge.kind, edge.line ?? null, edge.col ?? null);
193
+ }
194
+
195
+ /** Delete every edge whose source OR target belongs to a file. */
196
+ export function deleteEdgesForFile(db, filePath) {
197
+ db.prepare(`
198
+ DELETE FROM edges WHERE source IN (SELECT id FROM nodes WHERE file_path = ?)
199
+ OR target IN (SELECT id FROM nodes WHERE file_path = ?)
200
+ `).run(filePath, filePath);
201
+ }
202
+
203
+ /** Insert one unresolved cross-file reference (status 'pending'). */
204
+ export function insertUnresolvedRef(db, ref) {
205
+ db.prepare(`
206
+ INSERT INTO unresolved_refs(from_node_id, reference_name, reference_kind, line, col, file_path, module_specifier, status)
207
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')
208
+ `).run(ref.fromNodeId, ref.referenceName, ref.referenceKind, ref.line, ref.col, ref.filePath, ref.moduleSpecifier ?? null);
209
+ }
210
+
211
+ /** Delete every unresolved reference originating from a file. */
212
+ export function deleteUnresolvedRefsForFile(db, filePath) {
213
+ db.prepare("DELETE FROM unresolved_refs WHERE file_path = ?").run(filePath);
214
+ }
215
+
216
+ /** Summary counters used by /ast status. */
217
+ export function stats(db) {
218
+ const files = db.prepare("SELECT COUNT(*) AS count FROM files").get().count;
219
+ const nodes = db.prepare("SELECT COUNT(*) AS count FROM nodes").get().count;
220
+ const edges = db.prepare("SELECT COUNT(*) AS count FROM edges").get().count;
221
+ const unresolved = db.prepare("SELECT COUNT(*) AS count FROM unresolved_refs WHERE status = 'pending'").get().count;
222
+ return { files, nodes, edges, unresolved };
223
+ }
224
+
225
+ /** Store a provenance metadata value. */
226
+ export function setMetadata(db, key, value) {
227
+ db.prepare(`
228
+ INSERT INTO project_metadata(key, value, updated_at) VALUES (?, ?, ?)
229
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
230
+ `).run(key, value, Date.now());
231
+ }
232
+
233
+ /** Read a provenance metadata value, or undefined when absent. */
234
+ export function getMetadata(db, key) {
235
+ const row = db.prepare("SELECT value FROM project_metadata WHERE key = ?").get(key);
236
+ return row?.value;
237
+ }