aidimag 1.0.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 (91) hide show
  1. package/LICENSE +102 -0
  2. package/README.md +113 -0
  3. package/dist/capture/bootstrap.d.ts +25 -0
  4. package/dist/capture/bootstrap.js +188 -0
  5. package/dist/capture/commit-miner.d.ts +59 -0
  6. package/dist/capture/commit-miner.js +381 -0
  7. package/dist/capture/harvest.d.ts +50 -0
  8. package/dist/capture/harvest.js +207 -0
  9. package/dist/capture/pr-miner.d.ts +57 -0
  10. package/dist/capture/pr-miner.js +185 -0
  11. package/dist/capture/session-briefing.d.ts +36 -0
  12. package/dist/capture/session-briefing.js +150 -0
  13. package/dist/capture/session-extraction.d.ts +23 -0
  14. package/dist/capture/session-extraction.js +54 -0
  15. package/dist/capture/triage.d.ts +30 -0
  16. package/dist/capture/triage.js +108 -0
  17. package/dist/cli/commands/capture.d.ts +5 -0
  18. package/dist/cli/commands/capture.js +357 -0
  19. package/dist/cli/commands/hosts.d.ts +5 -0
  20. package/dist/cli/commands/hosts.js +98 -0
  21. package/dist/cli/commands/knowledge.d.ts +5 -0
  22. package/dist/cli/commands/knowledge.js +121 -0
  23. package/dist/cli/commands/memory.d.ts +6 -0
  24. package/dist/cli/commands/memory.js +392 -0
  25. package/dist/cli/commands/sync.d.ts +5 -0
  26. package/dist/cli/commands/sync.js +307 -0
  27. package/dist/cli/commands/tickets.d.ts +6 -0
  28. package/dist/cli/commands/tickets.js +328 -0
  29. package/dist/cli/commands/verify.d.ts +5 -0
  30. package/dist/cli/commands/verify.js +136 -0
  31. package/dist/cli/index.d.ts +19 -0
  32. package/dist/cli/index.js +46 -0
  33. package/dist/cli/shared.d.ts +37 -0
  34. package/dist/cli/shared.js +175 -0
  35. package/dist/config.d.ts +55 -0
  36. package/dist/config.js +51 -0
  37. package/dist/context/generate.d.ts +24 -0
  38. package/dist/context/generate.js +115 -0
  39. package/dist/critique/critique.d.ts +40 -0
  40. package/dist/critique/critique.js +110 -0
  41. package/dist/db/schema.d.ts +28 -0
  42. package/dist/db/schema.js +269 -0
  43. package/dist/db/store.d.ts +182 -0
  44. package/dist/db/store.js +906 -0
  45. package/dist/debug.d.ts +14 -0
  46. package/dist/debug.js +25 -0
  47. package/dist/embeddings/provider.d.ts +16 -0
  48. package/dist/embeddings/provider.js +100 -0
  49. package/dist/embeddings/search.d.ts +22 -0
  50. package/dist/embeddings/search.js +95 -0
  51. package/dist/index.d.ts +11 -0
  52. package/dist/index.js +12 -0
  53. package/dist/knowledge/chunk.d.ts +9 -0
  54. package/dist/knowledge/chunk.js +78 -0
  55. package/dist/knowledge/extract.d.ts +34 -0
  56. package/dist/knowledge/extract.js +113 -0
  57. package/dist/knowledge/ingest.d.ts +79 -0
  58. package/dist/knowledge/ingest.js +305 -0
  59. package/dist/knowledge/llm.d.ts +21 -0
  60. package/dist/knowledge/llm.js +110 -0
  61. package/dist/mcp/server.d.ts +11 -0
  62. package/dist/mcp/server.js +532 -0
  63. package/dist/security/evidence.d.ts +11 -0
  64. package/dist/security/evidence.js +15 -0
  65. package/dist/security/seal.d.ts +3 -0
  66. package/dist/security/seal.js +28 -0
  67. package/dist/security/sync-push.d.ts +2 -0
  68. package/dist/security/sync-push.js +37 -0
  69. package/dist/security/url.d.ts +6 -0
  70. package/dist/security/url.js +67 -0
  71. package/dist/sync/client.d.ts +84 -0
  72. package/dist/sync/client.js +391 -0
  73. package/dist/sync/server.d.ts +75 -0
  74. package/dist/sync/server.js +659 -0
  75. package/dist/tickets/provider.d.ts +80 -0
  76. package/dist/tickets/provider.js +375 -0
  77. package/dist/types.d.ts +133 -0
  78. package/dist/types.js +5 -0
  79. package/dist/ui/page.d.ts +5 -0
  80. package/dist/ui/page.js +841 -0
  81. package/dist/ui/server.d.ts +10 -0
  82. package/dist/ui/server.js +437 -0
  83. package/dist/verify/check.d.ts +38 -0
  84. package/dist/verify/check.js +121 -0
  85. package/dist/verify/engine.d.ts +39 -0
  86. package/dist/verify/engine.js +178 -0
  87. package/dist/verify/hooks.d.ts +24 -0
  88. package/dist/verify/hooks.js +125 -0
  89. package/dist/verify/runners.d.ts +26 -0
  90. package/dist/verify/runners.js +193 -0
  91. package/package.json +78 -0
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `dim ui` — local web dashboard (zero extra dependencies, node:http only).
3
+ *
4
+ * Serves a single-page UI: memory list with trust badges, proposal review
5
+ * queue, verify buttons, and a force-directed graph of memories ↔ scope paths
6
+ * (D3 from CDN). Works alongside any IDE; later IDE extensions can embed this
7
+ * same dashboard in a webview.
8
+ */
9
+ import type { MemoryStore } from "../db/store.js";
10
+ export declare function startUiServer(store: MemoryStore, repoRoot: string, port?: number): Promise<string>;
@@ -0,0 +1,437 @@
1
+ /**
2
+ * `dim ui` — local web dashboard (zero extra dependencies, node:http only).
3
+ *
4
+ * Serves a single-page UI: memory list with trust badges, proposal review
5
+ * queue, verify buttons, and a force-directed graph of memories ↔ scope paths
6
+ * (D3 from CDN). Works alongside any IDE; later IDE extensions can embed this
7
+ * same dashboard in a webview.
8
+ */
9
+ import { createServer } from "node:http";
10
+ import { watch, mkdirSync } from "node:fs";
11
+ import path from "node:path";
12
+ import { randomBytes, timingSafeEqual } from "node:crypto";
13
+ import { verifyAll } from "../verify/engine.js";
14
+ import { mineCommits } from "../capture/commit-miner.js";
15
+ import { hybridSearch, indexMemory, reindexAll } from "../embeddings/search.js";
16
+ import { readCloudConfig, writeCloudConfig, saveToken, getToken, sync as cloudSync } from "../sync/client.js";
17
+ import { resolveKnowledgeConfig } from "../config.js";
18
+ import { ingestAll } from "../knowledge/ingest.js";
19
+ import { isAllowedSyncServerUrl } from "../security/url.js";
20
+ import { readTicketsConfig, writeTicketsConfig, saveTicketCredential, getTicketCredential, ticketProviderFor, DEFAULT_TICKET_PATTERN, } from "../tickets/provider.js";
21
+ import { PAGE_HTML } from "./page.js";
22
+ function json(res, code, body) {
23
+ res.writeHead(code, { "Content-Type": "application/json" });
24
+ res.end(JSON.stringify(body));
25
+ }
26
+ function readBody(req) {
27
+ const MAX_BODY_BYTES = 5 * 1024 * 1024; // 5 MiB — bound memory use on POST bodies
28
+ return new Promise((resolve, reject) => {
29
+ let body = "";
30
+ let size = 0;
31
+ req.on("data", (d) => {
32
+ size += d.length;
33
+ if (size > MAX_BODY_BYTES) {
34
+ reject(new Error("request body too large"));
35
+ req.destroy();
36
+ return;
37
+ }
38
+ body += d;
39
+ });
40
+ req.on("end", () => {
41
+ try {
42
+ resolve(body ? JSON.parse(body) : {});
43
+ }
44
+ catch (err) {
45
+ reject(err);
46
+ }
47
+ });
48
+ req.on("error", reject);
49
+ });
50
+ }
51
+ export function startUiServer(store, repoRoot, port = 4517) {
52
+ const csrfToken = randomBytes(32).toString("base64url");
53
+ const isMutation = (method) => method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
54
+ const requireCsrf = (req) => {
55
+ const header = req.headers["x-aidimag-csrf-token"];
56
+ const got = (Array.isArray(header) ? header[0] : header) ?? "";
57
+ const a = Buffer.from(String(got));
58
+ const b = Buffer.from(csrfToken);
59
+ return a.length === b.length && a.length > 0 && timingSafeEqual(a, b);
60
+ };
61
+ // Auto-sync with the linked team server every N minutes while the dashboard
62
+ // runs (AIDIMAG_AUTOSYNC_MINUTES, default 10, 0 disables). Failures are
63
+ // silent — the next manual sync or dashboard action surfaces them.
64
+ const autoSyncMinutes = Number(process.env.AIDIMAG_AUTOSYNC_MINUTES ?? "10");
65
+ if (autoSyncMinutes > 0) {
66
+ const timer = setInterval(() => {
67
+ const cloud = readCloudConfig(repoRoot);
68
+ if (!cloud || !getToken(cloud.server))
69
+ return;
70
+ cloudSync(store, repoRoot).catch(() => undefined);
71
+ }, autoSyncMinutes * 60 * 1000);
72
+ timer.unref(); // never keep the process alive on its own
73
+ }
74
+ // Knowledge inbox watcher: auto-summarize docs dropped while the dashboard is up
75
+ // (the design's "automatic on drop while a long-running host is running" trigger).
76
+ // Best-effort and debounced; failures are silent, the next `dim knowledge sync` retries.
77
+ {
78
+ const cfg = resolveKnowledgeConfig(repoRoot);
79
+ const inbox = path.join(repoRoot, cfg.folder);
80
+ try {
81
+ mkdirSync(inbox, { recursive: true });
82
+ let running = false;
83
+ let queued = false;
84
+ let debounce;
85
+ const drain = async () => {
86
+ if (running) {
87
+ queued = true;
88
+ return;
89
+ }
90
+ running = true;
91
+ try {
92
+ const report = await ingestAll(store, repoRoot, cfg);
93
+ if (report.processed.length) {
94
+ console.log(`dim ui: ingested ${report.processed.length} knowledge doc(s) → review queue`);
95
+ }
96
+ }
97
+ catch { /* best-effort */ }
98
+ finally {
99
+ running = false;
100
+ if (queued) {
101
+ queued = false;
102
+ void drain();
103
+ }
104
+ }
105
+ };
106
+ const watcher = watch(inbox, { persistent: false }, () => {
107
+ if (debounce)
108
+ clearTimeout(debounce);
109
+ debounce = setTimeout(() => void drain(), 750);
110
+ });
111
+ watcher.unref?.();
112
+ void drain(); // catch up on anything already waiting
113
+ }
114
+ catch { /* watch unsupported on this platform — CLI sync still works */ }
115
+ }
116
+ const server = createServer(async (req, res) => {
117
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
118
+ const path = url.pathname;
119
+ try {
120
+ if (isMutation(req.method) && !requireCsrf(req)) {
121
+ json(res, 403, { error: "forbidden" });
122
+ return;
123
+ }
124
+ if (req.method === "GET" && path === "/") {
125
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
126
+ res.end(PAGE_HTML);
127
+ return;
128
+ }
129
+ if (req.method === "GET" && path === "/api/state") {
130
+ const cloud = readCloudConfig(repoRoot);
131
+ const tcfg = readTicketsConfig(repoRoot);
132
+ json(res, 200, {
133
+ repoRoot,
134
+ csrfToken,
135
+ memories: store.list(1000),
136
+ proposals: store.listProposals("PENDING", 200),
137
+ summary: store.statusSummary(),
138
+ cloud: cloud
139
+ ? { server: cloud.server, brain: cloud.brain, hasToken: !!getToken(cloud.server) }
140
+ : null,
141
+ tickets: tcfg.provider
142
+ ? {
143
+ provider: tcfg.provider,
144
+ baseUrl: tcfg.baseUrl ?? null,
145
+ pattern: tcfg.pattern ?? DEFAULT_TICKET_PATTERN,
146
+ hasCredential: tcfg.provider === "remote"
147
+ ? !!(cloud && getToken(cloud.server))
148
+ : !!getTicketCredential(tcfg.baseUrl ?? "linear"),
149
+ branch: tcfg.branch ?? null,
150
+ }
151
+ : null,
152
+ vecAvailable: store.vecAvailable,
153
+ });
154
+ return;
155
+ }
156
+ // ---- search (hybrid when embeddings configured) ----
157
+ if (req.method === "GET" && path === "/api/search") {
158
+ const { results, semantic } = await hybridSearch(store, {
159
+ query: url.searchParams.get("q") ?? "",
160
+ kind: url.searchParams.get("kind") || undefined,
161
+ paths: url.searchParams.get("path") ? [url.searchParams.get("path")] : undefined,
162
+ limit: 50,
163
+ includeRefuted: url.searchParams.get("all") === "1",
164
+ });
165
+ json(res, 200, { results, semantic });
166
+ return;
167
+ }
168
+ // ---- create memory (dim remember) ----
169
+ if (req.method === "POST" && path === "/api/memories") {
170
+ const b = await readBody(req);
171
+ if (!b.kind || !b.claim) {
172
+ json(res, 400, { error: "kind and claim are required" });
173
+ return;
174
+ }
175
+ const entry = store.write({
176
+ kind: b.kind,
177
+ claim: String(b.claim),
178
+ paths: b.paths ?? [],
179
+ symbols: b.symbols ?? [],
180
+ evidence: b.evidence ?? [],
181
+ createdBy: "human:dashboard",
182
+ pinned: Boolean(b.pinned),
183
+ guardrailLevel: b.guardrailLevel,
184
+ });
185
+ await indexMemory(store, entry).catch(() => false);
186
+ json(res, 201, { memory: entry });
187
+ return;
188
+ }
189
+ if (req.method === "POST" && path === "/api/verify") {
190
+ const deep = url.searchParams.get("deep") === "1";
191
+ json(res, 200, verifyAll(store, repoRoot, { deep }));
192
+ return;
193
+ }
194
+ // ---- mine git history ----
195
+ if (req.method === "POST" && path === "/api/mine") {
196
+ const full = url.searchParams.get("full") === "1";
197
+ const r = mineCommits(store, repoRoot, { full });
198
+ json(res, 200, {
199
+ scanned: r.scanned,
200
+ proposed: r.proposed.length,
201
+ skipped: r.skippedDuplicates,
202
+ noCommits: r.noCommits ?? false,
203
+ noNewCommits: r.noNewCommits ?? false,
204
+ cursor: r.lastSha,
205
+ });
206
+ return;
207
+ }
208
+ // ---- embeddings reindex ----
209
+ if (req.method === "POST" && path === "/api/reindex") {
210
+ const r = await reindexAll(store);
211
+ json(res, 200, {
212
+ indexed: r.indexed,
213
+ provider: r.provider ? `${r.provider.name}/${r.provider.model}` : null,
214
+ });
215
+ return;
216
+ }
217
+ // ---- team sync ----
218
+ if (req.method === "POST" && path === "/api/sync") {
219
+ const r = await cloudSync(store, repoRoot);
220
+ json(res, 200, r);
221
+ return;
222
+ }
223
+ // ---- cloud link/unlink ----
224
+ if (req.method === "POST" && path === "/api/cloud/link") {
225
+ const b = await readBody(req);
226
+ if (!b.server || !b.brain) {
227
+ json(res, 400, { error: "server and brain are required" });
228
+ return;
229
+ }
230
+ const serverUrl = String(b.server).replace(/\/$/, "");
231
+ if (!isAllowedSyncServerUrl(serverUrl)) {
232
+ json(res, 400, { error: "invalid server URL" });
233
+ return;
234
+ }
235
+ writeCloudConfig(repoRoot, { server: serverUrl, brain: String(b.brain) });
236
+ if (b.token)
237
+ saveToken(serverUrl, String(b.token));
238
+ json(res, 200, { ok: true, hasToken: !!getToken(serverUrl) });
239
+ return;
240
+ }
241
+ if (req.method === "POST" && path === "/api/cloud/unlink") {
242
+ writeCloudConfig(repoRoot, { server: "", brain: "" });
243
+ json(res, 200, { ok: true });
244
+ return;
245
+ }
246
+ // ---- tickets (T2 connect + T3 team share) ----
247
+ if (req.method === "POST" && path === "/api/tickets/connect") {
248
+ const b = await readBody(req);
249
+ const provider = String(b.provider ?? "");
250
+ if (!["jira", "github", "linear", "http", "remote"].includes(provider)) {
251
+ json(res, 400, { error: "provider must be jira | github | linear | http | remote" });
252
+ return;
253
+ }
254
+ const baseUrl = b.baseUrl ? String(b.baseUrl).replace(/\/$/, "") : undefined;
255
+ if (!baseUrl && !["linear", "remote"].includes(provider)) {
256
+ json(res, 400, { error: `baseUrl is required for ${provider}` });
257
+ return;
258
+ }
259
+ const existing = readTicketsConfig(repoRoot);
260
+ writeTicketsConfig(repoRoot, {
261
+ ...existing,
262
+ provider: provider,
263
+ baseUrl,
264
+ pattern: b.pattern ??
265
+ existing.pattern ??
266
+ (provider === "github" ? "#\\d+" : DEFAULT_TICKET_PATTERN),
267
+ });
268
+ if (b.token)
269
+ saveTicketCredential(baseUrl ?? "linear", String(b.token));
270
+ // trust-building: optional live validation round-trip
271
+ let validated = null;
272
+ if (b.testId) {
273
+ const p = ticketProviderFor(repoRoot);
274
+ const t = p ? await p.getTicket(String(b.testId)).catch(() => null) : null;
275
+ if (t)
276
+ validated = { id: t.id, title: t.title };
277
+ }
278
+ json(res, 200, { ok: true, validated });
279
+ return;
280
+ }
281
+ if (req.method === "POST" && path === "/api/tickets/disconnect") {
282
+ const existing = readTicketsConfig(repoRoot);
283
+ writeTicketsConfig(repoRoot, { branch: existing.branch }); // keep branch rules
284
+ json(res, 200, { ok: true });
285
+ return;
286
+ }
287
+ if (req.method === "GET" && path === "/api/tickets/show") {
288
+ const ticketId = url.searchParams.get("id");
289
+ if (!ticketId) {
290
+ json(res, 400, { error: "missing ?id=" });
291
+ return;
292
+ }
293
+ const p = ticketProviderFor(repoRoot);
294
+ if (!p) {
295
+ json(res, 400, { error: "no ticket provider connected (or credential missing)" });
296
+ return;
297
+ }
298
+ const t = await p.getTicket(ticketId);
299
+ if (!t)
300
+ json(res, 404, { error: `ticket ${ticketId} not found` });
301
+ else
302
+ json(res, 200, { ticket: t });
303
+ return;
304
+ }
305
+ // admin: push/remove team-shared credentials on the sync server
306
+ // (proxied like /api/keys — the admin token is per-request, never stored)
307
+ if (req.method === "POST" && path === "/api/tickets/share") {
308
+ const b = await readBody(req);
309
+ const cloud = readCloudConfig(repoRoot);
310
+ if (!cloud) {
311
+ json(res, 400, { error: "repo is not cloud-linked — link a team server first" });
312
+ return;
313
+ }
314
+ const admin = String(b.adminToken ?? "");
315
+ if (!admin) {
316
+ json(res, 400, { error: "adminToken is required" });
317
+ return;
318
+ }
319
+ const endpoint = `${cloud.server}/v1/ticket-config?brain=${encodeURIComponent(cloud.brain)}`;
320
+ const headers = { "Content-Type": "application/json", Authorization: `Bearer ${admin}` };
321
+ const upstream = b.remove
322
+ ? await fetch(endpoint, { method: "DELETE", headers })
323
+ : await fetch(endpoint, {
324
+ method: "PUT",
325
+ headers,
326
+ body: JSON.stringify({ provider: b.provider, baseUrl: b.baseUrl ?? "", credential: b.credential }),
327
+ });
328
+ json(res, upstream.status, await upstream.json());
329
+ return;
330
+ }
331
+ // ---- API key management (proxies to the sync server; admin token is
332
+ // passed per-request from the UI and never stored) ----
333
+ if (path === "/api/keys") {
334
+ const b = req.method === "GET" ? {} : await readBody(req);
335
+ const cloud = readCloudConfig(repoRoot);
336
+ // Admin token is sent in a header (or POST body) — never the query
337
+ // string — so it can't leak into browser history, proxy/access logs.
338
+ const headerToken = req.headers["x-aidimag-admin-token"];
339
+ const target = String(b.server ?? url.searchParams.get("server") ?? cloud?.server ?? "");
340
+ if (cloud && target && target.replace(/\/$/, "") !== cloud.server.replace(/\/$/, "")) {
341
+ json(res, 400, { error: "server must match linked cloud config" });
342
+ return;
343
+ }
344
+ const admin = String(b.adminToken ??
345
+ (Array.isArray(headerToken) ? headerToken[0] : headerToken) ??
346
+ "");
347
+ if (!target || !admin) {
348
+ json(res, 400, { error: "server and adminToken are required" });
349
+ return;
350
+ }
351
+ const headers = { "Content-Type": "application/json", Authorization: `Bearer ${admin}` };
352
+ let upstream;
353
+ if (req.method === "POST" && !b.revoke) {
354
+ upstream = await fetch(`${target}/v1/keys`, {
355
+ method: "POST",
356
+ headers,
357
+ body: JSON.stringify({ brain: b.brain, label: b.label }),
358
+ });
359
+ }
360
+ else if (req.method === "POST" && b.revoke) {
361
+ upstream = await fetch(`${target}/v1/keys?key=${encodeURIComponent(String(b.revoke))}`, {
362
+ method: "DELETE",
363
+ headers,
364
+ });
365
+ }
366
+ else {
367
+ upstream = await fetch(`${target}/v1/keys`, { headers });
368
+ }
369
+ json(res, upstream.status, await upstream.json());
370
+ return;
371
+ }
372
+ // POST /api/proposals/:id/(approve|reject)
373
+ const propMatch = path.match(/^\/api\/proposals\/([^/]+)\/(approve|reject)$/);
374
+ if (req.method === "POST" && propMatch) {
375
+ const [, id, action] = propMatch;
376
+ if (action === "approve") {
377
+ const memory = store.approveProposal(id);
378
+ await indexMemory(store, memory).catch(() => false);
379
+ json(res, 200, { memory });
380
+ }
381
+ else
382
+ json(res, 200, { proposal: store.rejectProposal(id) });
383
+ return;
384
+ }
385
+ // POST /api/memories/:id/(refute|forget|pin|unpin)
386
+ const memMatch = path.match(/^\/api\/memories\/([^/]+)\/(refute|forget|pin|unpin)$/);
387
+ if (req.method === "POST" && memMatch) {
388
+ const [, id, action] = memMatch;
389
+ const full = store.list(1000).find((m) => m.id === id || m.id.startsWith(id));
390
+ if (!full) {
391
+ json(res, 404, { error: `no memory ${id}` });
392
+ return;
393
+ }
394
+ if (action === "refute")
395
+ store.refute(full.id);
396
+ else if (action === "forget")
397
+ store.forget(full.id);
398
+ else
399
+ store.setPinned(full.id, action === "pin");
400
+ json(res, 200, { ok: true });
401
+ return;
402
+ }
403
+ json(res, 404, { error: "not found" });
404
+ }
405
+ catch (err) {
406
+ const msg = err instanceof Error ? err.message : "internal error";
407
+ console.error(`[dim ui] ${req.method} ${req.url}:`, msg);
408
+ json(res, 500, { error: msg });
409
+ }
410
+ });
411
+ return new Promise((resolve, reject) => {
412
+ const tryPort = (currentPort, maxAttempts = 10) => {
413
+ if (maxAttempts === 0) {
414
+ reject(new Error(`Could not find an available port after trying ${port}-${currentPort - 1}`));
415
+ return;
416
+ }
417
+ server.once("error", (err) => {
418
+ if (err.code === "EADDRINUSE") {
419
+ console.log(`⚠ Port ${currentPort} is already in use, trying ${currentPort + 1}...`);
420
+ server.removeAllListeners("error");
421
+ tryPort(currentPort + 1, maxAttempts - 1);
422
+ }
423
+ else {
424
+ reject(err);
425
+ }
426
+ });
427
+ server.listen(currentPort, "127.0.0.1", () => {
428
+ if (currentPort !== port) {
429
+ console.log(`ℹ Started on port ${currentPort} (requested port ${port} was in use)`);
430
+ }
431
+ resolve(`http://localhost:${currentPort}`);
432
+ });
433
+ };
434
+ tryPort(port);
435
+ });
436
+ }
437
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `dim check` (KARPATHY_LAYERS Feature 4) — Verifier layer, shifted left.
3
+ *
4
+ * Verification today runs *after* code lands (post-merge hook). `dim check`
5
+ * runs it against a diff BEFORE the commit, so contradictions are caught at
6
+ * author time. It analyzes `git diff --staged` (or an arbitrary ref) against
7
+ * the memories scoped to the changed files:
8
+ *
9
+ * - STATIC_CHECK evidence → re-run; a FAIL means the change broke the claim
10
+ * - GUARDRAIL (never) → keyword-match the added lines against the claim
11
+ * - INVARIANT / CONVENTION scoped to a changed file → advisory reminder
12
+ *
13
+ * Exit policy is the caller's: warn (exit 0) by default, block (exit 1) opt-in.
14
+ */
15
+ import type { MemoryStore } from "../db/store.js";
16
+ import type { MemoryEntry } from "../types.js";
17
+ export interface DiffFile {
18
+ path: string;
19
+ addedLines: string[];
20
+ }
21
+ export type Severity = "fail" | "warn";
22
+ export interface CheckViolation {
23
+ memory: MemoryEntry;
24
+ severity: Severity;
25
+ detail: string;
26
+ }
27
+ export interface CheckReport {
28
+ changedFiles: string[];
29
+ checked: number;
30
+ violations: CheckViolation[];
31
+ }
32
+ /** Parse a unified diff (—U0) into per-file added lines. */
33
+ export declare function parseDiff(diff: string): DiffFile[];
34
+ /** Run `git diff` for staged changes (default) or against a ref. */
35
+ export declare function gitDiff(repoRoot: string, ref?: string): string;
36
+ export declare function checkDiff(store: MemoryStore, repoRoot: string, opts?: {
37
+ ref?: string;
38
+ }): CheckReport;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * `dim check` (KARPATHY_LAYERS Feature 4) — Verifier layer, shifted left.
3
+ *
4
+ * Verification today runs *after* code lands (post-merge hook). `dim check`
5
+ * runs it against a diff BEFORE the commit, so contradictions are caught at
6
+ * author time. It analyzes `git diff --staged` (or an arbitrary ref) against
7
+ * the memories scoped to the changed files:
8
+ *
9
+ * - STATIC_CHECK evidence → re-run; a FAIL means the change broke the claim
10
+ * - GUARDRAIL (never) → keyword-match the added lines against the claim
11
+ * - INVARIANT / CONVENTION scoped to a changed file → advisory reminder
12
+ *
13
+ * Exit policy is the caller's: warn (exit 0) by default, block (exit 1) opt-in.
14
+ */
15
+ import { execFileSync } from "node:child_process";
16
+ import { runEvidence } from "./runners.js";
17
+ /** Parse a unified diff (—U0) into per-file added lines. */
18
+ export function parseDiff(diff) {
19
+ const files = [];
20
+ let current = null;
21
+ for (const line of diff.split("\n")) {
22
+ const m = line.match(/^\+\+\+ b\/(.+)$/);
23
+ if (m) {
24
+ current = { path: m[1], addedLines: [] };
25
+ if (m[1] !== "/dev/null")
26
+ files.push(current);
27
+ continue;
28
+ }
29
+ if (current && line.startsWith("+") && !line.startsWith("+++")) {
30
+ current.addedLines.push(line.slice(1));
31
+ }
32
+ }
33
+ return files;
34
+ }
35
+ /** Run `git diff` for staged changes (default) or against a ref. */
36
+ export function gitDiff(repoRoot, ref) {
37
+ const args = ref
38
+ ? ["diff", "--unified=0", `${ref}`, "--"]
39
+ : ["diff", "--cached", "--unified=0", "--"];
40
+ try {
41
+ return execFileSync("git", args, { cwd: repoRoot, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
42
+ }
43
+ catch {
44
+ return "";
45
+ }
46
+ }
47
+ const STOP = new Set(["the", "a", "an", "and", "or", "to", "of", "in", "on", "for", "with", "never", "always", "must", "only", "not", "use", "via"]);
48
+ function significantTokens(claim) {
49
+ return claim
50
+ .toLowerCase()
51
+ .split(/[^a-z0-9_./-]+/)
52
+ .filter((t) => t.length > 3 && !STOP.has(t));
53
+ }
54
+ /** A guardrail-never is "tripped" when added code contains enough of its key terms. */
55
+ function guardrailTripped(claim, addedText) {
56
+ const tokens = significantTokens(claim);
57
+ if (tokens.length === 0)
58
+ return false;
59
+ const hay = addedText.toLowerCase();
60
+ const hits = tokens.filter((t) => hay.includes(t)).length;
61
+ return hits / tokens.length >= 0.6;
62
+ }
63
+ export function checkDiff(store, repoRoot, opts = {}) {
64
+ const diff = gitDiff(repoRoot, opts.ref);
65
+ const diffFiles = parseDiff(diff);
66
+ const changedFiles = diffFiles.map((f) => f.path);
67
+ const addedByFile = new Map(diffFiles.map((f) => [f.path, f.addedLines.join("\n")]));
68
+ if (changedFiles.length === 0) {
69
+ return { changedFiles, checked: 0, violations: [] };
70
+ }
71
+ // Active memories scoped to the changed files (skip refuted/stale — only
72
+ // currently-trusted beliefs gate a commit).
73
+ const memories = store
74
+ .getForFiles(changedFiles, 200)
75
+ .filter((m) => m.status === "VERIFIED" || m.status === "UNVERIFIED" || m.pinned);
76
+ const violations = [];
77
+ const seenIds = new Set();
78
+ for (const m of memories) {
79
+ seenIds.add(m.id);
80
+ // 1) re-run any STATIC_CHECK evidence against the working tree
81
+ for (const ev of m.grounding) {
82
+ if (ev.type !== "STATIC_CHECK")
83
+ continue;
84
+ const outcome = runEvidence(ev, repoRoot);
85
+ if (outcome.result === "FAIL") {
86
+ violations.push({
87
+ memory: m,
88
+ severity: "fail",
89
+ detail: `STATIC_CHECK now fails (${outcome.detail}) — this change contradicts the claim`,
90
+ });
91
+ }
92
+ }
93
+ // 2) GUARDRAIL (never): pattern-match the added lines against the claim
94
+ if (m.kind === "GUARDRAIL" && m.guardrailLevel === "never") {
95
+ const added = m.scope.paths.length
96
+ ? m.scope.paths.flatMap((sp) => changedFiles.filter((f) => f.startsWith(sp) || sp.startsWith(f)).map((f) => addedByFile.get(f) ?? "")).join("\n")
97
+ : changedFiles.map((f) => addedByFile.get(f) ?? "").join("\n");
98
+ if (guardrailTripped(m.claim, added)) {
99
+ violations.push({
100
+ memory: m,
101
+ severity: "fail",
102
+ detail: "🚫 NEVER guardrail: the staged change appears to do exactly what this forbids",
103
+ });
104
+ }
105
+ }
106
+ // 3) INVARIANT / CONVENTION scoped to a changed file → advisory reminder
107
+ if ((m.kind === "INVARIANT" || m.kind === "CONVENTION") && m.scope.paths.length) {
108
+ const touches = m.scope.paths.some((sp) => changedFiles.some((f) => f.startsWith(sp) || sp.startsWith(f)));
109
+ const hasStaticCheck = m.grounding.some((e) => e.type === "STATIC_CHECK");
110
+ if (touches && !hasStaticCheck) {
111
+ violations.push({
112
+ memory: m,
113
+ severity: "warn",
114
+ detail: `${m.kind} covers a file you changed — make sure it still holds (no automated check attached)`,
115
+ });
116
+ }
117
+ }
118
+ }
119
+ return { changedFiles, checked: seenIds.size, violations };
120
+ }
121
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Verification engine (Phase 3) — runs evidence and applies the
3
+ * status lifecycle: UNVERIFIED/VERIFIED ↔ STALE. REFUTED stays a
4
+ * deliberate human/agent action, never automatic.
5
+ *
6
+ * Transition rules per memory:
7
+ * - any runnable evidence FAILs → STALE (confidence floored)
8
+ * - all runnable evidence PASSes (≥1) → VERIFIED (confidence boosted)
9
+ * - only skipped/unknown evidence, or none → status unchanged
10
+ * - REFUTED memories are never re-verified (negative knowledge is final
11
+ * until explicitly superseded)
12
+ */
13
+ import { type RunOptions, type RunOutcome } from "./runners.js";
14
+ import type { MemoryStore } from "../db/store.js";
15
+ import type { MemoryEntry, MemoryStatus } from "../types.js";
16
+ export interface MemoryVerification {
17
+ memoryId: string;
18
+ claim: string;
19
+ before: MemoryStatus;
20
+ after: MemoryStatus;
21
+ confidenceBefore: number;
22
+ confidenceAfter: number;
23
+ outcomes: RunOutcome[];
24
+ decayed?: boolean;
25
+ }
26
+ export interface VerifyReport {
27
+ checked: number;
28
+ verified: number;
29
+ stale: number;
30
+ unchanged: number;
31
+ decayed: number;
32
+ results: MemoryVerification[];
33
+ }
34
+ export declare function decayedConfidence(confidence: number, lastAnchorIso: string, halfLifeDays: number, now?: Date): number;
35
+ export declare function verifyMemory(store: MemoryStore, memory: MemoryEntry, repoRoot: string, opts?: RunOptions): MemoryVerification;
36
+ export declare function verifyAll(store: MemoryStore, repoRoot: string, opts?: {
37
+ ids?: string[];
38
+ deep?: boolean;
39
+ }): VerifyReport;