@folterung/project-memory 0.1.0

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 (215) hide show
  1. package/.memignore.example +11 -0
  2. package/README.md +48 -0
  3. package/docker-compose.yml +17 -0
  4. package/package.json +36 -0
  5. package/packages/cli/bin/mem.js +6 -0
  6. package/packages/cli/coverage/lcov-report/base.css +224 -0
  7. package/packages/cli/coverage/lcov-report/block-navigation.js +87 -0
  8. package/packages/cli/coverage/lcov-report/chunking/chunker.ts.html +538 -0
  9. package/packages/cli/coverage/lcov-report/chunking/hash.ts.html +148 -0
  10. package/packages/cli/coverage/lcov-report/chunking/index.html +146 -0
  11. package/packages/cli/coverage/lcov-report/chunking/types.ts.html +214 -0
  12. package/packages/cli/coverage/lcov-report/config/index.html +131 -0
  13. package/packages/cli/coverage/lcov-report/config/load.ts.html +184 -0
  14. package/packages/cli/coverage/lcov-report/config/types.ts.html +232 -0
  15. package/packages/cli/coverage/lcov-report/embedding/index.html +116 -0
  16. package/packages/cli/coverage/lcov-report/embedding/stub.ts.html +181 -0
  17. package/packages/cli/coverage/lcov-report/favicon.png +0 -0
  18. package/packages/cli/coverage/lcov-report/index.html +161 -0
  19. package/packages/cli/coverage/lcov-report/prettify.css +1 -0
  20. package/packages/cli/coverage/lcov-report/prettify.js +2 -0
  21. package/packages/cli/coverage/lcov-report/scope/allowlist.ts.html +199 -0
  22. package/packages/cli/coverage/lcov-report/scope/ignore.ts.html +343 -0
  23. package/packages/cli/coverage/lcov-report/scope/index.html +131 -0
  24. package/packages/cli/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  25. package/packages/cli/coverage/lcov-report/sorter.js +210 -0
  26. package/packages/cli/coverage/lcov.info +669 -0
  27. package/packages/cli/coverage/tmp/coverage-25917-1770055893226-0.json +1 -0
  28. package/packages/cli/coverage/tmp/coverage-25918-1770055893272-0.json +1 -0
  29. package/packages/cli/coverage/tmp/coverage-25919-1770055893273-0.json +1 -0
  30. package/packages/cli/coverage/tmp/coverage-25920-1770055893271-0.json +1 -0
  31. package/packages/cli/coverage/tmp/coverage-25921-1770055893279-0.json +1 -0
  32. package/packages/cli/coverage/tmp/coverage-25922-1770055893272-0.json +1 -0
  33. package/packages/cli/coverage/tmp/coverage-25923-1770055893275-0.json +1 -0
  34. package/packages/cli/coverage/tmp/coverage-25924-1770055893294-0.json +1 -0
  35. package/packages/cli/coverage/tmp/coverage-25925-1770055893290-0.json +1 -0
  36. package/packages/cli/dist/chunking/chunker.d.ts +2 -0
  37. package/packages/cli/dist/chunking/chunker.js +142 -0
  38. package/packages/cli/dist/chunking/chunker.js.map +1 -0
  39. package/packages/cli/dist/chunking/chunker.test.d.ts +1 -0
  40. package/packages/cli/dist/chunking/chunker.test.js +50 -0
  41. package/packages/cli/dist/chunking/chunker.test.js.map +1 -0
  42. package/packages/cli/dist/chunking/hash.d.ts +3 -0
  43. package/packages/cli/dist/chunking/hash.js +17 -0
  44. package/packages/cli/dist/chunking/hash.js.map +1 -0
  45. package/packages/cli/dist/chunking/hash.test.d.ts +1 -0
  46. package/packages/cli/dist/chunking/hash.test.js +36 -0
  47. package/packages/cli/dist/chunking/hash.test.js.map +1 -0
  48. package/packages/cli/dist/chunking/types.d.ts +19 -0
  49. package/packages/cli/dist/chunking/types.js +24 -0
  50. package/packages/cli/dist/chunking/types.js.map +1 -0
  51. package/packages/cli/dist/chunking/types.test.d.ts +1 -0
  52. package/packages/cli/dist/chunking/types.test.js +25 -0
  53. package/packages/cli/dist/chunking/types.test.js.map +1 -0
  54. package/packages/cli/dist/cli/index.d.ts +1 -0
  55. package/packages/cli/dist/cli/index.js +67 -0
  56. package/packages/cli/dist/cli/index.js.map +1 -0
  57. package/packages/cli/dist/commands/deep-index.d.ts +1 -0
  58. package/packages/cli/dist/commands/deep-index.js +17 -0
  59. package/packages/cli/dist/commands/deep-index.js.map +1 -0
  60. package/packages/cli/dist/commands/doctor.d.ts +1 -0
  61. package/packages/cli/dist/commands/doctor.js +27 -0
  62. package/packages/cli/dist/commands/doctor.js.map +1 -0
  63. package/packages/cli/dist/commands/down.d.ts +1 -0
  64. package/packages/cli/dist/commands/down.js +13 -0
  65. package/packages/cli/dist/commands/down.js.map +1 -0
  66. package/packages/cli/dist/commands/explain.d.ts +1 -0
  67. package/packages/cli/dist/commands/explain.js +23 -0
  68. package/packages/cli/dist/commands/explain.js.map +1 -0
  69. package/packages/cli/dist/commands/init.d.ts +1 -0
  70. package/packages/cli/dist/commands/init.js +35 -0
  71. package/packages/cli/dist/commands/init.js.map +1 -0
  72. package/packages/cli/dist/commands/query.d.ts +1 -0
  73. package/packages/cli/dist/commands/query.js +44 -0
  74. package/packages/cli/dist/commands/query.js.map +1 -0
  75. package/packages/cli/dist/commands/reset.d.ts +3 -0
  76. package/packages/cli/dist/commands/reset.js +28 -0
  77. package/packages/cli/dist/commands/reset.js.map +1 -0
  78. package/packages/cli/dist/commands/scaffold.d.ts +1 -0
  79. package/packages/cli/dist/commands/scaffold.js +52 -0
  80. package/packages/cli/dist/commands/scaffold.js.map +1 -0
  81. package/packages/cli/dist/commands/update.d.ts +5 -0
  82. package/packages/cli/dist/commands/update.js +23 -0
  83. package/packages/cli/dist/commands/update.js.map +1 -0
  84. package/packages/cli/dist/commands/watch.d.ts +1 -0
  85. package/packages/cli/dist/commands/watch.js +46 -0
  86. package/packages/cli/dist/commands/watch.js.map +1 -0
  87. package/packages/cli/dist/config/defaults.d.ts +2 -0
  88. package/packages/cli/dist/config/defaults.js +47 -0
  89. package/packages/cli/dist/config/defaults.js.map +1 -0
  90. package/packages/cli/dist/config/load.d.ts +4 -0
  91. package/packages/cli/dist/config/load.js +29 -0
  92. package/packages/cli/dist/config/load.js.map +1 -0
  93. package/packages/cli/dist/config/load.test.d.ts +1 -0
  94. package/packages/cli/dist/config/load.test.js +51 -0
  95. package/packages/cli/dist/config/load.test.js.map +1 -0
  96. package/packages/cli/dist/config/types.d.ts +32 -0
  97. package/packages/cli/dist/config/types.js +24 -0
  98. package/packages/cli/dist/config/types.js.map +1 -0
  99. package/packages/cli/dist/config/types.test.d.ts +1 -0
  100. package/packages/cli/dist/config/types.test.js +31 -0
  101. package/packages/cli/dist/config/types.test.js.map +1 -0
  102. package/packages/cli/dist/docker/compose.d.ts +5 -0
  103. package/packages/cli/dist/docker/compose.js +75 -0
  104. package/packages/cli/dist/docker/compose.js.map +1 -0
  105. package/packages/cli/dist/embedding/stub.d.ts +6 -0
  106. package/packages/cli/dist/embedding/stub.js +29 -0
  107. package/packages/cli/dist/embedding/stub.js.map +1 -0
  108. package/packages/cli/dist/embedding/stub.test.d.ts +1 -0
  109. package/packages/cli/dist/embedding/stub.test.js +31 -0
  110. package/packages/cli/dist/embedding/stub.test.js.map +1 -0
  111. package/packages/cli/dist/phase1/pipeline.d.ts +1 -0
  112. package/packages/cli/dist/phase1/pipeline.js +145 -0
  113. package/packages/cli/dist/phase1/pipeline.js.map +1 -0
  114. package/packages/cli/dist/phase2/deep-index.d.ts +1 -0
  115. package/packages/cli/dist/phase2/deep-index.js +105 -0
  116. package/packages/cli/dist/phase2/deep-index.js.map +1 -0
  117. package/packages/cli/dist/qdrant/upsert.d.ts +14 -0
  118. package/packages/cli/dist/qdrant/upsert.js +30 -0
  119. package/packages/cli/dist/qdrant/upsert.js.map +1 -0
  120. package/packages/cli/dist/scope/allowlist.d.ts +11 -0
  121. package/packages/cli/dist/scope/allowlist.js +36 -0
  122. package/packages/cli/dist/scope/allowlist.js.map +1 -0
  123. package/packages/cli/dist/scope/allowlist.test.d.ts +1 -0
  124. package/packages/cli/dist/scope/allowlist.test.js +23 -0
  125. package/packages/cli/dist/scope/allowlist.test.js.map +1 -0
  126. package/packages/cli/dist/scope/ignore.d.ts +2 -0
  127. package/packages/cli/dist/scope/ignore.js +80 -0
  128. package/packages/cli/dist/scope/ignore.js.map +1 -0
  129. package/packages/cli/dist/scope/ignore.test.d.ts +1 -0
  130. package/packages/cli/dist/scope/ignore.test.js +71 -0
  131. package/packages/cli/dist/scope/ignore.test.js.map +1 -0
  132. package/packages/cli/package.json +46 -0
  133. package/packages/cli/src/chunking/chunker.test.ts +54 -0
  134. package/packages/cli/src/chunking/chunker.ts +151 -0
  135. package/packages/cli/src/chunking/hash.test.ts +41 -0
  136. package/packages/cli/src/chunking/hash.ts +21 -0
  137. package/packages/cli/src/chunking/types.test.ts +28 -0
  138. package/packages/cli/src/chunking/types.ts +43 -0
  139. package/packages/cli/src/cli/index.ts +79 -0
  140. package/packages/cli/src/commands/deep-index.ts +16 -0
  141. package/packages/cli/src/commands/doctor.ts +32 -0
  142. package/packages/cli/src/commands/down.ts +12 -0
  143. package/packages/cli/src/commands/explain.ts +22 -0
  144. package/packages/cli/src/commands/init.ts +37 -0
  145. package/packages/cli/src/commands/query.ts +49 -0
  146. package/packages/cli/src/commands/reset.ts +30 -0
  147. package/packages/cli/src/commands/scaffold.ts +55 -0
  148. package/packages/cli/src/commands/update.ts +22 -0
  149. package/packages/cli/src/commands/watch.ts +47 -0
  150. package/packages/cli/src/config/defaults.ts +49 -0
  151. package/packages/cli/src/config/load.test.ts +55 -0
  152. package/packages/cli/src/config/load.ts +33 -0
  153. package/packages/cli/src/config/types.test.ts +43 -0
  154. package/packages/cli/src/config/types.ts +49 -0
  155. package/packages/cli/src/docker/compose.ts +75 -0
  156. package/packages/cli/src/embedding/stub.test.ts +35 -0
  157. package/packages/cli/src/embedding/stub.ts +32 -0
  158. package/packages/cli/src/phase1/pipeline.ts +164 -0
  159. package/packages/cli/src/phase2/deep-index.ts +120 -0
  160. package/packages/cli/src/qdrant/upsert.ts +45 -0
  161. package/packages/cli/src/scope/allowlist.test.ts +25 -0
  162. package/packages/cli/src/scope/allowlist.ts +38 -0
  163. package/packages/cli/src/scope/ignore.test.ts +71 -0
  164. package/packages/cli/src/scope/ignore.ts +86 -0
  165. package/packages/cli/tsconfig.json +16 -0
  166. package/packages/server/coverage/lcov-report/base.css +224 -0
  167. package/packages/server/coverage/lcov-report/block-navigation.js +87 -0
  168. package/packages/server/coverage/lcov-report/favicon.png +0 -0
  169. package/packages/server/coverage/lcov-report/index.html +116 -0
  170. package/packages/server/coverage/lcov-report/prettify.css +1 -0
  171. package/packages/server/coverage/lcov-report/prettify.js +2 -0
  172. package/packages/server/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  173. package/packages/server/coverage/lcov-report/sorter.js +210 -0
  174. package/packages/server/coverage/lcov-report/stub.ts.html +184 -0
  175. package/packages/server/coverage/lcov.info +57 -0
  176. package/packages/server/coverage/tmp/coverage-26012-1770055894042-0.json +1 -0
  177. package/packages/server/coverage/tmp/coverage-26013-1770055894077-0.json +1 -0
  178. package/packages/server/dist/api/explain.d.ts +6 -0
  179. package/packages/server/dist/api/explain.js +16 -0
  180. package/packages/server/dist/api/explain.js.map +1 -0
  181. package/packages/server/dist/api/health.d.ts +7 -0
  182. package/packages/server/dist/api/health.js +43 -0
  183. package/packages/server/dist/api/health.js.map +1 -0
  184. package/packages/server/dist/api/refresh.d.ts +2 -0
  185. package/packages/server/dist/api/refresh.js +8 -0
  186. package/packages/server/dist/api/refresh.js.map +1 -0
  187. package/packages/server/dist/api/search.d.ts +28 -0
  188. package/packages/server/dist/api/search.js +36 -0
  189. package/packages/server/dist/api/search.js.map +1 -0
  190. package/packages/server/dist/api/system.d.ts +6 -0
  191. package/packages/server/dist/api/system.js +17 -0
  192. package/packages/server/dist/api/system.js.map +1 -0
  193. package/packages/server/dist/embedding/stub.d.ts +7 -0
  194. package/packages/server/dist/embedding/stub.js +30 -0
  195. package/packages/server/dist/embedding/stub.js.map +1 -0
  196. package/packages/server/dist/embedding/stub.test.d.ts +1 -0
  197. package/packages/server/dist/embedding/stub.test.js +25 -0
  198. package/packages/server/dist/embedding/stub.test.js.map +1 -0
  199. package/packages/server/dist/index.d.ts +1 -0
  200. package/packages/server/dist/index.js +26 -0
  201. package/packages/server/dist/index.js.map +1 -0
  202. package/packages/server/dist/qdrant/client.d.ts +24 -0
  203. package/packages/server/dist/qdrant/client.js +55 -0
  204. package/packages/server/dist/qdrant/client.js.map +1 -0
  205. package/packages/server/package.json +37 -0
  206. package/packages/server/src/api/explain.ts +24 -0
  207. package/packages/server/src/api/health.ts +50 -0
  208. package/packages/server/src/api/refresh.ts +9 -0
  209. package/packages/server/src/api/search.ts +57 -0
  210. package/packages/server/src/api/system.ts +25 -0
  211. package/packages/server/src/embedding/stub.test.ts +28 -0
  212. package/packages/server/src/embedding/stub.ts +33 -0
  213. package/packages/server/src/index.ts +29 -0
  214. package/packages/server/src/qdrant/client.ts +92 -0
  215. package/packages/server/tsconfig.json +16 -0
@@ -0,0 +1,120 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import chalk from "chalk";
4
+ import { loadConfig } from "../config/load.js";
5
+ import { chunkFile } from "../chunking/chunker.js";
6
+ import type { Chunk } from "../chunking/types.js";
7
+ import { embedBatch } from "../embedding/stub.js";
8
+ import { createQdrantClient, ensureCollection, upsertPoints } from "../qdrant/upsert.js";
9
+ import { listInScopeFiles } from "../scope/allowlist.js";
10
+ import { createIgnoreFilter } from "../scope/ignore.js";
11
+ import { toRelativePath } from "../scope/allowlist.js";
12
+ import { DEEP_INDEX_STATE_FILE } from "../config/types.js";
13
+
14
+ const BATCH_SIZE = 8;
15
+ const BATCH_DELAY_MS = 200;
16
+ const PHASE1_KINDS = new Set(["system_map", "module_summary", "entrypoint"]);
17
+
18
+ interface DeepIndexState {
19
+ embeddedIds: string[];
20
+ total: number;
21
+ lastUpdated: string;
22
+ }
23
+
24
+ function loadDeepIndexState(cwd: string): DeepIndexState {
25
+ const path = join(cwd, DEEP_INDEX_STATE_FILE);
26
+ if (!existsSync(path)) return { embeddedIds: [], total: 0, lastUpdated: "" };
27
+ try {
28
+ const raw = readFileSync(path, "utf8");
29
+ const data = JSON.parse(raw) as { embeddedIds?: string[]; total?: number; lastUpdated?: string };
30
+ return {
31
+ embeddedIds: data.embeddedIds ?? [],
32
+ total: data.total ?? 0,
33
+ lastUpdated: data.lastUpdated ?? "",
34
+ };
35
+ } catch {
36
+ return { embeddedIds: [], total: 0, lastUpdated: "" };
37
+ }
38
+ }
39
+
40
+ function saveDeepIndexState(cwd: string, state: DeepIndexState): void {
41
+ const stateDir = join(cwd, ".mem", "state");
42
+ if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });
43
+ writeFileSync(join(cwd, DEEP_INDEX_STATE_FILE), JSON.stringify(state, null, 2), "utf8");
44
+ }
45
+
46
+ function sleep(ms: number): Promise<void> {
47
+ return new Promise((r) => setTimeout(r, ms));
48
+ }
49
+
50
+ export async function runDeepIndex(cwd: string): Promise<void> {
51
+ const config = loadConfig(cwd);
52
+ const isIgnored = createIgnoreFilter(cwd, config.indexing?.exclude ?? []);
53
+ const inScopePaths = listInScopeFiles(cwd, config);
54
+ const eligiblePaths: string[] = [];
55
+ for (const abs of inScopePaths) {
56
+ const rel = toRelativePath(cwd, abs);
57
+ if (!isIgnored(rel)) eligiblePaths.push(abs);
58
+ }
59
+
60
+ const allChunks: Chunk[] = [];
61
+ for (const abs of eligiblePaths) {
62
+ try {
63
+ const content = readFileSync(abs, "utf8");
64
+ const chunks = chunkFile(toRelativePath(cwd, abs), content);
65
+ allChunks.push(...chunks);
66
+ } catch {
67
+ // skip
68
+ }
69
+ }
70
+
71
+ let state = loadDeepIndexState(cwd);
72
+ if (!state.embeddedIds.length && state.total === 0) {
73
+ state = { embeddedIds: [], total: allChunks.length, lastUpdated: "" };
74
+ }
75
+ const embeddedSet = new Set(state.embeddedIds);
76
+ const remaining = allChunks.filter((c) => !embeddedSet.has(c.id));
77
+
78
+ if (remaining.length === 0) {
79
+ console.log(chalk.green("Deep index already complete."));
80
+ return;
81
+ }
82
+
83
+ const host = config.qdrant?.host ?? "127.0.0.1";
84
+ const port = config.qdrant?.port ?? 6333;
85
+ const collection = config.qdrant?.collection ?? "project_memory";
86
+ const client = createQdrantClient(host, port);
87
+ await ensureCollection(client, collection);
88
+
89
+ let embeddedCount = state.embeddedIds.length;
90
+ const total = allChunks.length;
91
+
92
+ for (let i = 0; i < remaining.length; i += BATCH_SIZE) {
93
+ const batch = remaining.slice(i, i + BATCH_SIZE);
94
+ const texts = batch.map((c) => c.text.slice(0, 8000));
95
+ const vectors = embedBatch(texts);
96
+ const points = batch.map((c, j) => ({
97
+ id: c.id,
98
+ vector: vectors[j],
99
+ payload: {
100
+ kind: "chunk",
101
+ path: c.metadata.path,
102
+ text: texts[j],
103
+ },
104
+ }));
105
+ await upsertPoints(client, collection, points);
106
+ embeddedCount += batch.length;
107
+ const newEmbeddedIds = [...state.embeddedIds, ...batch.map((c) => c.id)];
108
+ state.embeddedIds = newEmbeddedIds;
109
+ state.total = total;
110
+ state.lastUpdated = new Date().toISOString();
111
+ saveDeepIndexState(cwd, state);
112
+
113
+ const percent = total > 0 ? Math.round((embeddedCount / total) * 100) : 0;
114
+ console.log(chalk.dim(` Deep index: ${embeddedCount}/${total} (${percent}%)`));
115
+
116
+ await sleep(BATCH_DELAY_MS);
117
+ }
118
+
119
+ console.log(chalk.green("✓"), "Deep index complete:", embeddedCount, "chunks");
120
+ }
@@ -0,0 +1,45 @@
1
+ import { QdrantClient } from "@qdrant/js-client-rest";
2
+ import { getVectorSize } from "../embedding/stub.js";
3
+
4
+ const DEFAULT_COLLECTION = "project_memory";
5
+
6
+ export function createQdrantClient(host: string = "127.0.0.1", port: number = 6333): QdrantClient {
7
+ return new QdrantClient({ url: `http://${host}:${port}` });
8
+ }
9
+
10
+ export async function ensureCollection(
11
+ client: QdrantClient,
12
+ collectionName: string = DEFAULT_COLLECTION
13
+ ): Promise<void> {
14
+ const size = getVectorSize();
15
+ try {
16
+ await client.getCollection(collectionName);
17
+ } catch {
18
+ await client.createCollection(collectionName, {
19
+ vectors: { size, distance: "Cosine" },
20
+ });
21
+ }
22
+ }
23
+
24
+ export interface PointPayload {
25
+ kind: string;
26
+ path?: string;
27
+ text: string;
28
+ [key: string]: unknown;
29
+ }
30
+
31
+ export async function upsertPoints(
32
+ client: QdrantClient,
33
+ collectionName: string,
34
+ points: { id: string; vector: number[]; payload: PointPayload }[]
35
+ ): Promise<void> {
36
+ if (points.length === 0) return;
37
+ await client.upsert(collectionName, {
38
+ wait: true,
39
+ points: points.map((p) => ({
40
+ id: p.id,
41
+ vector: p.vector,
42
+ payload: p.payload,
43
+ })),
44
+ });
45
+ }
@@ -0,0 +1,25 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import { toRelativePath } from "./allowlist.js";
4
+
5
+ describe("scope allowlist", () => {
6
+ it("toRelativePath strips cwd and normalizes slashes", () => {
7
+ const cwd = "/home/repo";
8
+ const abs = "/home/repo/src/foo.ts";
9
+ assert.strictEqual(toRelativePath(cwd, abs), "src/foo.ts");
10
+ });
11
+
12
+ it("toRelativePath handles trailing slash in cwd", () => {
13
+ const cwd = "/home/repo/";
14
+ const abs = "/home/repo/src/foo.ts";
15
+ assert.strictEqual(toRelativePath(cwd, abs), "src/foo.ts");
16
+ });
17
+
18
+ it("toRelativePath uses forward slashes", () => {
19
+ const cwd = "C:\\repo";
20
+ const abs = "C:\\repo\\src\\foo.ts";
21
+ const rel = toRelativePath(cwd, abs);
22
+ assert.ok(!rel.includes("\\"));
23
+ assert.strictEqual(rel, "src/foo.ts");
24
+ });
25
+ });
@@ -0,0 +1,38 @@
1
+ import fg from "fast-glob";
2
+ import { join } from "node:path";
3
+ import type { MemConfig } from "../config/types.js";
4
+
5
+ /**
6
+ * Resolve include patterns to a list of absolute paths that are in scope.
7
+ * Apply BEFORE any directory traversal. Only these paths are eligible.
8
+ * If indexing.include is absent, entire repo root is eligible (we still need to list files).
9
+ */
10
+ export function listInScopeFiles(cwd: string, config: MemConfig): string[] {
11
+ const include = config.indexing?.include;
12
+ if (!include?.length) {
13
+ const all = fg.sync("**/*", {
14
+ cwd,
15
+ dot: true,
16
+ onlyFiles: true,
17
+ absolute: true,
18
+ suppressErrors: true,
19
+ });
20
+ return all.map((p) => p.startsWith(cwd) ? p : join(cwd, p));
21
+ }
22
+ const files = fg.sync(include, {
23
+ cwd,
24
+ dot: true,
25
+ onlyFiles: true,
26
+ absolute: true,
27
+ suppressErrors: true,
28
+ });
29
+ return files.map((p) => (p.startsWith(cwd) ? p : join(cwd, p)));
30
+ }
31
+
32
+ /**
33
+ * Given an absolute path, return the relative path from cwd for ignore matching.
34
+ */
35
+ export function toRelativePath(cwd: string, absolutePath: string): string {
36
+ const rel = absolutePath.slice(cwd.length).replace(/^[/\\]+/, "");
37
+ return rel.replace(/\\/g, "/");
38
+ }
@@ -0,0 +1,71 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { createIgnoreFilter, getBuiltinIgnores } from "./ignore.js";
7
+
8
+ describe("scope ignore", () => {
9
+ it("getBuiltinIgnores includes node_modules and .env", () => {
10
+ const builtin = getBuiltinIgnores();
11
+ assert.ok(builtin.some((p) => p.includes("node_modules")), "builtin should ignore node_modules");
12
+ assert.ok(builtin.includes(".env"));
13
+ });
14
+
15
+ it("createIgnoreFilter ignores node_modules", () => {
16
+ const cwd = mkdtempSync(join(tmpdir(), "mem-ignore-"));
17
+ try {
18
+ const isIgnored = createIgnoreFilter(cwd, []);
19
+ assert.strictEqual(isIgnored("node_modules/foo/bar.js"), true);
20
+ assert.strictEqual(isIgnored("src/foo.js"), false);
21
+ } finally {
22
+ rmSync(cwd, { recursive: true, force: true });
23
+ }
24
+ });
25
+
26
+ it("createIgnoreFilter ignores .env", () => {
27
+ const cwd = mkdtempSync(join(tmpdir(), "mem-ignore-"));
28
+ try {
29
+ const isIgnored = createIgnoreFilter(cwd, []);
30
+ assert.strictEqual(isIgnored(".env"), true);
31
+ assert.strictEqual(isIgnored(".env.local"), true);
32
+ } finally {
33
+ rmSync(cwd, { recursive: true, force: true });
34
+ }
35
+ });
36
+
37
+ it("createIgnoreFilter respects .memignore when present", () => {
38
+ const cwd = mkdtempSync(join(tmpdir(), "mem-ignore-"));
39
+ writeFileSync(join(cwd, ".memignore"), "*.skip\n", "utf8");
40
+ try {
41
+ const isIgnored = createIgnoreFilter(cwd, []);
42
+ assert.strictEqual(isIgnored("foo.skip"), true);
43
+ assert.strictEqual(isIgnored("foo.ts"), false);
44
+ } finally {
45
+ rmSync(cwd, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ it("createIgnoreFilter negation re-includes", () => {
50
+ const cwd = mkdtempSync(join(tmpdir(), "mem-ignore-"));
51
+ writeFileSync(join(cwd, ".memignore"), "*.skip\n!important.skip\n", "utf8");
52
+ try {
53
+ const isIgnored = createIgnoreFilter(cwd, []);
54
+ assert.strictEqual(isIgnored("foo.skip"), true);
55
+ assert.strictEqual(isIgnored("important.skip"), false);
56
+ } finally {
57
+ rmSync(cwd, { recursive: true, force: true });
58
+ }
59
+ });
60
+
61
+ it("createIgnoreFilter respects config exclude", () => {
62
+ const cwd = mkdtempSync(join(tmpdir(), "mem-ignore-"));
63
+ try {
64
+ const isIgnored = createIgnoreFilter(cwd, ["**/excluded/**"]);
65
+ assert.strictEqual(isIgnored("src/excluded/bar.js"), true);
66
+ assert.strictEqual(isIgnored("src/other/bar.js"), false);
67
+ } finally {
68
+ rmSync(cwd, { recursive: true, force: true });
69
+ }
70
+ });
71
+ });
@@ -0,0 +1,86 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { Minimatch } from "minimatch";
4
+ import { MEMIGNORE_FILE } from "../config/types.js";
5
+
6
+ const BUILTIN_IGNORES = [
7
+ "node_modules/**",
8
+ "vendor/**",
9
+ "dist/**",
10
+ "build/**",
11
+ ".next/**",
12
+ ".cache/**",
13
+ "**/*.png",
14
+ "**/*.jpg",
15
+ "**/*.jpeg",
16
+ "**/*.gif",
17
+ "**/*.mp4",
18
+ "**/*.zip",
19
+ ".env",
20
+ ".env.*",
21
+ "**/*.pem",
22
+ "**/*.key",
23
+ "**/id_rsa",
24
+ ".mem/",
25
+ ];
26
+
27
+ function parseIgnoreLines(content: string): { pattern: string; negated: boolean }[] {
28
+ const out: { pattern: string; negated: boolean }[] = [];
29
+ for (const line of content.split(/\r?\n/)) {
30
+ const trimmed = line.trim();
31
+ if (!trimmed || trimmed.startsWith("#")) continue;
32
+ const negated = trimmed.startsWith("!");
33
+ const pattern = negated ? trimmed.slice(1).trim() : trimmed;
34
+ if (pattern) out.push({ pattern, negated });
35
+ }
36
+ return out;
37
+ }
38
+
39
+ function loadMemignore(cwd: string): { pattern: string; negated: boolean }[] {
40
+ const path = join(cwd, MEMIGNORE_FILE);
41
+ if (!existsSync(path)) return [];
42
+ const content = readFileSync(path, "utf8");
43
+ return parseIgnoreLines(content);
44
+ }
45
+
46
+ function loadConfigExclude(cwd: string, excludePatterns: string[]): { pattern: string; negated: boolean }[] {
47
+ if (!excludePatterns?.length) return [];
48
+ return excludePatterns.map((pattern) => ({ pattern, negated: false }));
49
+ }
50
+
51
+ function buildMatcher(pattern: string): (p: string) => boolean {
52
+ const m = new Minimatch(pattern, { dot: true, matchBase: !pattern.includes("/") });
53
+ return (p: string) => m.match(p);
54
+ }
55
+
56
+ export function createIgnoreFilter(
57
+ cwd: string,
58
+ configExclude: string[] = []
59
+ ): (relativePath: string) => boolean {
60
+ const builtinRules: { pattern: string; negated: boolean }[] = BUILTIN_IGNORES.map((pattern) => ({
61
+ pattern,
62
+ negated: false,
63
+ }));
64
+ const memignoreRules = loadMemignore(cwd);
65
+ const configRules = loadConfigExclude(cwd, configExclude);
66
+ const ordered: { match: (p: string) => boolean; negated: boolean }[] = [
67
+ ...builtinRules.map((r) => ({ match: buildMatcher(r.pattern), negated: r.negated })),
68
+ ...memignoreRules.map((r) => ({ match: buildMatcher(r.pattern), negated: r.negated })),
69
+ ...configRules.map((r) => ({ match: buildMatcher(r.pattern), negated: r.negated })),
70
+ ];
71
+
72
+ return function isIgnored(relativePath: string): boolean {
73
+ const normalized = relativePath.replace(/\\/g, "/");
74
+ let ignored = false;
75
+ for (const { match, negated } of ordered) {
76
+ if (match(normalized)) {
77
+ ignored = !negated;
78
+ }
79
+ }
80
+ return ignored;
81
+ };
82
+ }
83
+
84
+ export function getBuiltinIgnores(): string[] {
85
+ return [...BUILTIN_IGNORES];
86
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "declaration": true,
12
+ "sourceMap": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }
@@ -0,0 +1,224 @@
1
+ body, html {
2
+ margin:0; padding: 0;
3
+ height: 100%;
4
+ }
5
+ body {
6
+ font-family: Helvetica Neue, Helvetica, Arial;
7
+ font-size: 14px;
8
+ color:#333;
9
+ }
10
+ .small { font-size: 12px; }
11
+ *, *:after, *:before {
12
+ -webkit-box-sizing:border-box;
13
+ -moz-box-sizing:border-box;
14
+ box-sizing:border-box;
15
+ }
16
+ h1 { font-size: 20px; margin: 0;}
17
+ h2 { font-size: 14px; }
18
+ pre {
19
+ font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
20
+ margin: 0;
21
+ padding: 0;
22
+ -moz-tab-size: 2;
23
+ -o-tab-size: 2;
24
+ tab-size: 2;
25
+ }
26
+ a { color:#0074D9; text-decoration:none; }
27
+ a:hover { text-decoration:underline; }
28
+ .strong { font-weight: bold; }
29
+ .space-top1 { padding: 10px 0 0 0; }
30
+ .pad2y { padding: 20px 0; }
31
+ .pad1y { padding: 10px 0; }
32
+ .pad2x { padding: 0 20px; }
33
+ .pad2 { padding: 20px; }
34
+ .pad1 { padding: 10px; }
35
+ .space-left2 { padding-left:55px; }
36
+ .space-right2 { padding-right:20px; }
37
+ .center { text-align:center; }
38
+ .clearfix { display:block; }
39
+ .clearfix:after {
40
+ content:'';
41
+ display:block;
42
+ height:0;
43
+ clear:both;
44
+ visibility:hidden;
45
+ }
46
+ .fl { float: left; }
47
+ @media only screen and (max-width:640px) {
48
+ .col3 { width:100%; max-width:100%; }
49
+ .hide-mobile { display:none!important; }
50
+ }
51
+
52
+ .quiet {
53
+ color: #7f7f7f;
54
+ color: rgba(0,0,0,0.5);
55
+ }
56
+ .quiet a { opacity: 0.7; }
57
+
58
+ .fraction {
59
+ font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
60
+ font-size: 10px;
61
+ color: #555;
62
+ background: #E8E8E8;
63
+ padding: 4px 5px;
64
+ border-radius: 3px;
65
+ vertical-align: middle;
66
+ }
67
+
68
+ div.path a:link, div.path a:visited { color: #333; }
69
+ table.coverage {
70
+ border-collapse: collapse;
71
+ margin: 10px 0 0 0;
72
+ padding: 0;
73
+ }
74
+
75
+ table.coverage td {
76
+ margin: 0;
77
+ padding: 0;
78
+ vertical-align: top;
79
+ }
80
+ table.coverage td.line-count {
81
+ text-align: right;
82
+ padding: 0 5px 0 20px;
83
+ }
84
+ table.coverage td.line-coverage {
85
+ text-align: right;
86
+ padding-right: 10px;
87
+ min-width:20px;
88
+ }
89
+
90
+ table.coverage td span.cline-any {
91
+ display: inline-block;
92
+ padding: 0 5px;
93
+ width: 100%;
94
+ }
95
+ .missing-if-branch {
96
+ display: inline-block;
97
+ margin-right: 5px;
98
+ border-radius: 3px;
99
+ position: relative;
100
+ padding: 0 4px;
101
+ background: #333;
102
+ color: yellow;
103
+ }
104
+
105
+ .skip-if-branch {
106
+ display: none;
107
+ margin-right: 10px;
108
+ position: relative;
109
+ padding: 0 4px;
110
+ background: #ccc;
111
+ color: white;
112
+ }
113
+ .missing-if-branch .typ, .skip-if-branch .typ {
114
+ color: inherit !important;
115
+ }
116
+ .coverage-summary {
117
+ border-collapse: collapse;
118
+ width: 100%;
119
+ }
120
+ .coverage-summary tr { border-bottom: 1px solid #bbb; }
121
+ .keyline-all { border: 1px solid #ddd; }
122
+ .coverage-summary td, .coverage-summary th { padding: 10px; }
123
+ .coverage-summary tbody { border: 1px solid #bbb; }
124
+ .coverage-summary td { border-right: 1px solid #bbb; }
125
+ .coverage-summary td:last-child { border-right: none; }
126
+ .coverage-summary th {
127
+ text-align: left;
128
+ font-weight: normal;
129
+ white-space: nowrap;
130
+ }
131
+ .coverage-summary th.file { border-right: none !important; }
132
+ .coverage-summary th.pct { }
133
+ .coverage-summary th.pic,
134
+ .coverage-summary th.abs,
135
+ .coverage-summary td.pct,
136
+ .coverage-summary td.abs { text-align: right; }
137
+ .coverage-summary td.file { white-space: nowrap; }
138
+ .coverage-summary td.pic { min-width: 120px !important; }
139
+ .coverage-summary tfoot td { }
140
+
141
+ .coverage-summary .sorter {
142
+ height: 10px;
143
+ width: 7px;
144
+ display: inline-block;
145
+ margin-left: 0.5em;
146
+ background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
147
+ }
148
+ .coverage-summary .sorted .sorter {
149
+ background-position: 0 -20px;
150
+ }
151
+ .coverage-summary .sorted-desc .sorter {
152
+ background-position: 0 -10px;
153
+ }
154
+ .status-line { height: 10px; }
155
+ /* yellow */
156
+ .cbranch-no { background: yellow !important; color: #111; }
157
+ /* dark red */
158
+ .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
159
+ .low .chart { border:1px solid #C21F39 }
160
+ .highlighted,
161
+ .highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
162
+ background: #C21F39 !important;
163
+ }
164
+ /* medium red */
165
+ .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
166
+ /* light red */
167
+ .low, .cline-no { background:#FCE1E5 }
168
+ /* light green */
169
+ .high, .cline-yes { background:rgb(230,245,208) }
170
+ /* medium green */
171
+ .cstat-yes { background:rgb(161,215,106) }
172
+ /* dark green */
173
+ .status-line.high, .high .cover-fill { background:rgb(77,146,33) }
174
+ .high .chart { border:1px solid rgb(77,146,33) }
175
+ /* dark yellow (gold) */
176
+ .status-line.medium, .medium .cover-fill { background: #f9cd0b; }
177
+ .medium .chart { border:1px solid #f9cd0b; }
178
+ /* light yellow */
179
+ .medium { background: #fff4c2; }
180
+
181
+ .cstat-skip { background: #ddd; color: #111; }
182
+ .fstat-skip { background: #ddd; color: #111 !important; }
183
+ .cbranch-skip { background: #ddd !important; color: #111; }
184
+
185
+ span.cline-neutral { background: #eaeaea; }
186
+
187
+ .coverage-summary td.empty {
188
+ opacity: .5;
189
+ padding-top: 4px;
190
+ padding-bottom: 4px;
191
+ line-height: 1;
192
+ color: #888;
193
+ }
194
+
195
+ .cover-fill, .cover-empty {
196
+ display:inline-block;
197
+ height: 12px;
198
+ }
199
+ .chart {
200
+ line-height: 0;
201
+ }
202
+ .cover-empty {
203
+ background: white;
204
+ }
205
+ .cover-full {
206
+ border-right: none !important;
207
+ }
208
+ pre.prettyprint {
209
+ border: none !important;
210
+ padding: 0 !important;
211
+ margin: 0 !important;
212
+ }
213
+ .com { color: #999 !important; }
214
+ .ignore-none { color: #999; font-weight: normal; }
215
+
216
+ .wrapper {
217
+ min-height: 100%;
218
+ height: auto !important;
219
+ height: 100%;
220
+ margin: 0 auto -48px;
221
+ }
222
+ .footer, .push {
223
+ height: 48px;
224
+ }