@hiveai/core 0.13.8 → 0.14.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.
package/dist/index.js CHANGED
@@ -791,6 +791,102 @@ function summarizeImpact(scores) {
791
791
  return summary;
792
792
  }
793
793
 
794
+ // src/prevention.ts
795
+ import { appendFile, mkdir as mkdir2, readFile as readFile4 } from "fs/promises";
796
+ import { existsSync as existsSync4 } from "fs";
797
+ import path6 from "path";
798
+ function preventionLogPath(paths) {
799
+ return path6.join(paths.haiveDir, ".cache", "prevention-log.jsonl");
800
+ }
801
+ async function appendPreventionEvent(paths, event) {
802
+ const file = preventionLogPath(paths);
803
+ await mkdir2(path6.dirname(file), { recursive: true });
804
+ await appendFile(file, JSON.stringify(event) + "\n", "utf8");
805
+ }
806
+ async function loadPreventionEvents(paths) {
807
+ const file = preventionLogPath(paths);
808
+ if (!existsSync4(file)) return [];
809
+ const raw = await readFile4(file, "utf8").catch(() => "");
810
+ const events = [];
811
+ for (const line of raw.split("\n")) {
812
+ const trimmed = line.trim();
813
+ if (!trimmed) continue;
814
+ try {
815
+ const e = JSON.parse(trimmed);
816
+ if (e && typeof e.at === "string" && typeof e.id === "string") events.push(e);
817
+ } catch {
818
+ }
819
+ }
820
+ return events;
821
+ }
822
+ var MS_PER_DAY2 = 864e5;
823
+ function computePreventionTrend(events, now = /* @__PURE__ */ new Date(), weeks = 6) {
824
+ const nowMs = now.getTime();
825
+ let last7 = 0;
826
+ let last30 = 0;
827
+ const weekly = new Array(weeks).fill(0);
828
+ for (const e of events) {
829
+ const t = Date.parse(e.at);
830
+ if (!Number.isFinite(t)) continue;
831
+ const ageDays = (nowMs - t) / MS_PER_DAY2;
832
+ if (ageDays < 0) continue;
833
+ if (ageDays <= 7) last7 += 1;
834
+ if (ageDays <= 30) last30 += 1;
835
+ const weekIdx = weeks - 1 - Math.floor(ageDays / 7);
836
+ if (weekIdx >= 0 && weekIdx < weeks) weekly[weekIdx] = (weekly[weekIdx] ?? 0) + 1;
837
+ }
838
+ return { last_7d: last7, last_30d: last30, weekly };
839
+ }
840
+ function computeRecurrence(events) {
841
+ const byId = /* @__PURE__ */ new Map();
842
+ for (const e of events) {
843
+ const cur = byId.get(e.id) ?? { catches: 0, days: /* @__PURE__ */ new Set(), last: e.at };
844
+ cur.catches += 1;
845
+ cur.days.add(e.at.slice(0, 10));
846
+ if (e.at > cur.last) cur.last = e.at;
847
+ byId.set(e.id, cur);
848
+ }
849
+ const rows = [];
850
+ for (const [id, v] of byId) {
851
+ if (v.days.size >= 2) {
852
+ rows.push({ id, catches: v.catches, distinct_days: v.days.size, last_at: v.last });
853
+ }
854
+ }
855
+ rows.sort((a, b) => b.distinct_days - a.distinct_days || b.catches - a.catches);
856
+ return { recurring_count: rows.length, top: rows };
857
+ }
858
+
859
+ // src/context-throttle.ts
860
+ import { createHash } from "crypto";
861
+ import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
862
+ import { existsSync as existsSync5 } from "fs";
863
+ import path7 from "path";
864
+ var PROJECT_CONTEXT_THROTTLE_MS = 8 * 60 * 1e3;
865
+ function throttleMarkerPath(paths) {
866
+ return path7.join(paths.haiveDir, ".cache", "briefing-context.json");
867
+ }
868
+ function hashProjectContext(content) {
869
+ return createHash("sha1").update(content).digest("hex").slice(0, 16);
870
+ }
871
+ async function projectContextRecentlyEmitted(paths, hash, now = Date.now()) {
872
+ const file = throttleMarkerPath(paths);
873
+ if (!existsSync5(file)) return false;
874
+ try {
875
+ const m = JSON.parse(await readFile5(file, "utf8"));
876
+ if (m.hash !== hash || !m.at) return false;
877
+ return now - Date.parse(m.at) < PROJECT_CONTEXT_THROTTLE_MS;
878
+ } catch {
879
+ return false;
880
+ }
881
+ }
882
+ async function recordProjectContextEmission(paths, hash, now = Date.now()) {
883
+ const file = throttleMarkerPath(paths);
884
+ await mkdir3(path7.dirname(file), { recursive: true }).catch(() => {
885
+ });
886
+ await writeFile2(file, JSON.stringify({ hash, at: new Date(now).toISOString() }), "utf8").catch(() => {
887
+ });
888
+ }
889
+
794
890
  // src/eval.ts
795
891
  function round32(n) {
796
892
  return Math.round(n * 1e3) / 1e3;
@@ -926,13 +1022,13 @@ var DEFAULT_CONFIDENCE_THRESHOLDS = {
926
1022
  decayDays: 180,
927
1023
  hardDecayDays: 365
928
1024
  };
929
- var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
1025
+ var MS_PER_DAY3 = 24 * 60 * 60 * 1e3;
930
1026
  function deriveConfidence(fm, usage, thresholds = DEFAULT_CONFIDENCE_THRESHOLDS, now = /* @__PURE__ */ new Date()) {
931
1027
  if (fm.status === "stale" || fm.status === "deprecated" || fm.status === "rejected") return "stale";
932
1028
  const baseLevel = baseConfidence(fm, usage, thresholds);
933
1029
  if (baseLevel !== "authoritative" && baseLevel !== "trusted") return baseLevel;
934
1030
  const anchor = usage.last_read_at ?? fm.created_at;
935
- const ageDays = (now.getTime() - new Date(anchor).getTime()) / MS_PER_DAY2;
1031
+ const ageDays = (now.getTime() - new Date(anchor).getTime()) / MS_PER_DAY3;
936
1032
  if (Number.isNaN(ageDays) || ageDays <= 0) return baseLevel;
937
1033
  if (ageDays >= thresholds.hardDecayDays) {
938
1034
  return "low";
@@ -1226,10 +1322,10 @@ function allocateBudget(parts, maxTokens) {
1226
1322
  }
1227
1323
 
1228
1324
  // src/code-map.ts
1229
- import { mkdir as mkdir2, readFile as readFile4, readdir as readdir3, writeFile as writeFile2 } from "fs/promises";
1230
- import { existsSync as existsSync4 } from "fs";
1325
+ import { mkdir as mkdir4, readFile as readFile6, readdir as readdir3, writeFile as writeFile3 } from "fs/promises";
1326
+ import { existsSync as existsSync6 } from "fs";
1231
1327
  import { spawnSync } from "child_process";
1232
- import path6 from "path";
1328
+ import path8 from "path";
1233
1329
  var CODE_MAP_FILE = "code-map.json";
1234
1330
  var DEFAULT_INCLUDE = [
1235
1331
  ".ts",
@@ -1271,27 +1367,27 @@ var DEFAULT_EXCLUDE = [
1271
1367
  ];
1272
1368
  var TEST_FILE_RE = /\.(test|spec)\.[a-z]+$/i;
1273
1369
  function codeMapPath(paths) {
1274
- return path6.join(paths.haiveDir, CODE_MAP_FILE);
1370
+ return path8.join(paths.haiveDir, CODE_MAP_FILE);
1275
1371
  }
1276
1372
  async function loadCodeMap(paths) {
1277
1373
  const file = codeMapPath(paths);
1278
- if (!existsSync4(file)) return null;
1279
- return JSON.parse(await readFile4(file, "utf8"));
1374
+ if (!existsSync6(file)) return null;
1375
+ return JSON.parse(await readFile6(file, "utf8"));
1280
1376
  }
1281
1377
  async function saveCodeMap(paths, map) {
1282
1378
  const file = codeMapPath(paths);
1283
- await mkdir2(path6.dirname(file), { recursive: true });
1284
- await writeFile2(file, JSON.stringify(map, null, 2), "utf8");
1379
+ await mkdir4(path8.dirname(file), { recursive: true });
1380
+ await writeFile3(file, JSON.stringify(map, null, 2), "utf8");
1285
1381
  }
1286
1382
  async function buildCodeMap(root, options = {}) {
1287
1383
  const include = new Set(options.includeExtensions ?? DEFAULT_INCLUDE);
1288
1384
  const exclude = new Set(options.excludeDirs ?? DEFAULT_EXCLUDE);
1289
1385
  const files = {};
1290
1386
  for await (const abs of collectSourceFiles(root, include, exclude, options.includeUntracked)) {
1291
- const rel = path6.relative(root, abs).replace(/\\/g, "/");
1387
+ const rel = path8.relative(root, abs).replace(/\\/g, "/");
1292
1388
  if (rel.startsWith(".ai/")) continue;
1293
- const content = await readFile4(abs, "utf8");
1294
- const ext = path6.extname(abs).toLowerCase();
1389
+ const content = await readFile6(abs, "utf8");
1390
+ const ext = path8.extname(abs).toLowerCase();
1295
1391
  const entry = parseFile(content, ext);
1296
1392
  if (entry.exports.length > 0) files[rel] = entry;
1297
1393
  }
@@ -1305,7 +1401,7 @@ async function buildCodeMap(root, options = {}) {
1305
1401
  async function* collectSourceFiles(root, include, exclude, includeUntracked) {
1306
1402
  const gitFiles = gitSourceFiles(root, include, exclude, includeUntracked === true);
1307
1403
  if (gitFiles) {
1308
- for (const rel of gitFiles) yield path6.join(root, rel);
1404
+ for (const rel of gitFiles) yield path8.join(root, rel);
1309
1405
  return;
1310
1406
  }
1311
1407
  yield* walkSourceFiles(root, include, exclude);
@@ -1332,11 +1428,11 @@ async function* walkSourceFiles(dir, include, exclude) {
1332
1428
  if (entry.isDirectory()) continue;
1333
1429
  }
1334
1430
  if (exclude.has(entry.name)) continue;
1335
- const full = path6.join(dir, entry.name);
1431
+ const full = path8.join(dir, entry.name);
1336
1432
  if (entry.isDirectory()) {
1337
1433
  yield* walkSourceFiles(full, include, exclude);
1338
1434
  } else if (entry.isFile()) {
1339
- const ext = path6.extname(entry.name).toLowerCase();
1435
+ const ext = path8.extname(entry.name).toLowerCase();
1340
1436
  if (include.has(ext) && !TEST_FILE_RE.test(entry.name)) yield full;
1341
1437
  }
1342
1438
  }
@@ -1347,7 +1443,7 @@ function isIncludedSourcePath(rel, include, exclude) {
1347
1443
  const parts = normalized.split("/");
1348
1444
  if (parts.some((part) => exclude.has(part))) return false;
1349
1445
  const base = parts.at(-1) ?? "";
1350
- const ext = path6.extname(base).toLowerCase();
1446
+ const ext = path8.extname(base).toLowerCase();
1351
1447
  return include.has(ext) && !TEST_FILE_RE.test(base);
1352
1448
  }
1353
1449
  var EXPORT_RE = /^export\s+(?:default\s+)?(async\s+)?(function|class|interface|type|const|let|var|enum)\s+(\*?)\s*([A-Za-z_$][\w$]*)/gm;
@@ -1561,10 +1657,10 @@ function queryCodeMap(map, options) {
1561
1657
  }
1562
1658
 
1563
1659
  // src/config.ts
1564
- import { existsSync as existsSync5 } from "fs";
1660
+ import { existsSync as existsSync7 } from "fs";
1565
1661
  import { readFileSync } from "fs";
1566
- import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
1567
- import path7 from "path";
1662
+ import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
1663
+ import path9 from "path";
1568
1664
  var CONFIG_FILE = "haive.config.json";
1569
1665
  var DEFAULT_CONFIG = {
1570
1666
  autopilot: false,
@@ -1636,13 +1732,13 @@ function antiPatternGateParams(gate) {
1636
1732
  }
1637
1733
  }
1638
1734
  function configPath(paths) {
1639
- return path7.join(paths.haiveDir, CONFIG_FILE);
1735
+ return path9.join(paths.haiveDir, CONFIG_FILE);
1640
1736
  }
1641
1737
  async function loadConfig(paths) {
1642
1738
  const file = configPath(paths);
1643
- if (!existsSync5(file)) return { ...DEFAULT_CONFIG };
1739
+ if (!existsSync7(file)) return { ...DEFAULT_CONFIG };
1644
1740
  try {
1645
- const raw = await readFile5(file, "utf8");
1741
+ const raw = await readFile7(file, "utf8");
1646
1742
  const parsed = JSON.parse(raw);
1647
1743
  const merged = mergeConfig(DEFAULT_CONFIG, parsed);
1648
1744
  if (merged.autopilot) {
@@ -1655,7 +1751,7 @@ async function loadConfig(paths) {
1655
1751
  }
1656
1752
  function loadConfigSync(paths) {
1657
1753
  const file = configPath(paths);
1658
- if (!existsSync5(file)) return { ...DEFAULT_CONFIG };
1754
+ if (!existsSync7(file)) return { ...DEFAULT_CONFIG };
1659
1755
  try {
1660
1756
  const parsed = JSON.parse(readFileSync(file, "utf8"));
1661
1757
  const merged = mergeConfig(DEFAULT_CONFIG, parsed);
@@ -1665,7 +1761,7 @@ function loadConfigSync(paths) {
1665
1761
  }
1666
1762
  }
1667
1763
  async function saveConfig(paths, config) {
1668
- await writeFile3(configPath(paths), JSON.stringify(config, null, 2) + "\n", "utf8");
1764
+ await writeFile4(configPath(paths), JSON.stringify(config, null, 2) + "\n", "utf8");
1669
1765
  }
1670
1766
  function mergeConfig(base, override) {
1671
1767
  return {
@@ -1683,21 +1779,21 @@ function mergeConfig(base, override) {
1683
1779
  }
1684
1780
 
1685
1781
  // src/cross-repo.ts
1686
- import { existsSync as existsSync6 } from "fs";
1687
- import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
1688
- import path8 from "path";
1782
+ import { existsSync as existsSync8 } from "fs";
1783
+ import { mkdir as mkdir5, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
1784
+ import path10 from "path";
1689
1785
  import { spawnSync as spawnSync2 } from "child_process";
1690
1786
  async function loadImportMap(cacheDir) {
1691
- const mapPath = path8.join(cacheDir, "import-map.json");
1692
- if (!existsSync6(mapPath)) return {};
1787
+ const mapPath = path10.join(cacheDir, "import-map.json");
1788
+ if (!existsSync8(mapPath)) return {};
1693
1789
  try {
1694
- return JSON.parse(await readFile6(mapPath, "utf8"));
1790
+ return JSON.parse(await readFile8(mapPath, "utf8"));
1695
1791
  } catch {
1696
1792
  return {};
1697
1793
  }
1698
1794
  }
1699
1795
  async function saveImportMap(cacheDir, map) {
1700
- await writeFile4(path8.join(cacheDir, "import-map.json"), JSON.stringify(map, null, 2) + "\n", "utf8");
1796
+ await writeFile5(path10.join(cacheDir, "import-map.json"), JSON.stringify(map, null, 2) + "\n", "utf8");
1701
1797
  }
1702
1798
  async function pullCrossRepoSources(paths, config, projectRoot) {
1703
1799
  const sources = config.crossRepoSources ?? [];
@@ -1718,8 +1814,8 @@ async function pullFromSource(paths, source, projectRoot) {
1718
1814
  };
1719
1815
  let sourceRoot = null;
1720
1816
  if (source.path) {
1721
- const resolved = path8.resolve(projectRoot, source.path);
1722
- if (!existsSync6(resolved)) {
1817
+ const resolved = path10.resolve(projectRoot, source.path);
1818
+ if (!existsSync8(resolved)) {
1723
1819
  report.errors.push(`Path not found: ${resolved}`);
1724
1820
  return report;
1725
1821
  }
@@ -1732,7 +1828,7 @@ async function pullFromSource(paths, source, projectRoot) {
1732
1828
  return report;
1733
1829
  }
1734
1830
  const sourcePaths = resolveHaivePaths(sourceRoot);
1735
- if (!existsSync6(sourcePaths.memoriesDir)) {
1831
+ if (!existsSync8(sourcePaths.memoriesDir)) {
1736
1832
  report.errors.push(`No .ai/memories/ found at ${sourceRoot}`);
1737
1833
  return report;
1738
1834
  }
@@ -1755,10 +1851,10 @@ async function pullFromSource(paths, source, projectRoot) {
1755
1851
  report.skipped.push("no shared memories found in source");
1756
1852
  return report;
1757
1853
  }
1758
- const destDir = path8.join(paths.memoriesDir, "shared", source.name);
1759
- await mkdir3(destDir, { recursive: true });
1760
- const cacheDir = path8.join(paths.haiveDir, ".cache", "cross-repo", source.name);
1761
- await mkdir3(cacheDir, { recursive: true });
1854
+ const destDir = path10.join(paths.memoriesDir, "shared", source.name);
1855
+ await mkdir5(destDir, { recursive: true });
1856
+ const cacheDir = path10.join(paths.haiveDir, ".cache", "cross-repo", source.name);
1857
+ await mkdir5(cacheDir, { recursive: true });
1762
1858
  const importMap = await loadImportMap(cacheDir);
1763
1859
  const mapDirty = false;
1764
1860
  let dirty = mapDirty;
@@ -1772,7 +1868,7 @@ async function pullFromSource(paths, source, projectRoot) {
1772
1868
 
1773
1869
  `;
1774
1870
  const existingLocalPath = importMap[sourceId];
1775
- if (existingLocalPath && existsSync6(existingLocalPath)) {
1871
+ if (existingLocalPath && existsSync8(existingLocalPath)) {
1776
1872
  const existingFiles = await loadMemoriesFromDir(destDir);
1777
1873
  const existingEntry = existingFiles.find(({ filePath }) => filePath === existingLocalPath);
1778
1874
  const sourceBodyStripped = memory.body.trim();
@@ -1783,7 +1879,7 @@ async function pullFromSource(paths, source, projectRoot) {
1783
1879
  }
1784
1880
  const updatedBody = importedBodyPrefix + memory.body;
1785
1881
  if (existingEntry) {
1786
- await writeFile4(
1882
+ await writeFile5(
1787
1883
  existingLocalPath,
1788
1884
  serializeMemory({ frontmatter: existingEntry.memory.frontmatter, body: updatedBody }),
1789
1885
  "utf8"
@@ -1807,8 +1903,8 @@ async function pullFromSource(paths, source, projectRoot) {
1807
1903
  topic: fm.topic ? `${source.name}:${fm.topic}` : void 0
1808
1904
  });
1809
1905
  const body = importedBodyPrefix + memory.body;
1810
- const destPath = path8.join(destDir, `${newFm.id}.md`);
1811
- await writeFile4(destPath, serializeMemory({ frontmatter: newFm, body }), "utf8");
1906
+ const destPath = path10.join(destDir, `${newFm.id}.md`);
1907
+ await writeFile5(destPath, serializeMemory({ frontmatter: newFm, body }), "utf8");
1812
1908
  importMap[sourceId] = destPath;
1813
1909
  dirty = true;
1814
1910
  report.imported.push(sourceId);
@@ -1818,9 +1914,9 @@ async function pullFromSource(paths, source, projectRoot) {
1818
1914
  return report;
1819
1915
  }
1820
1916
  async function cloneOrFetchGitSource(source, paths, report) {
1821
- const cacheDir = path8.join(paths.haiveDir, ".cache", "cross-repo", source.name);
1822
- await mkdir3(cacheDir, { recursive: true });
1823
- if (existsSync6(path8.join(cacheDir, ".git"))) {
1917
+ const cacheDir = path10.join(paths.haiveDir, ".cache", "cross-repo", source.name);
1918
+ await mkdir5(cacheDir, { recursive: true });
1919
+ if (existsSync8(path10.join(cacheDir, ".git"))) {
1824
1920
  const result = spawnSync2("git", ["fetch", "--depth=1", "origin"], {
1825
1921
  cwd: cacheDir,
1826
1922
  encoding: "utf8"
@@ -1845,9 +1941,9 @@ async function cloneOrFetchGitSource(source, paths, report) {
1845
1941
  }
1846
1942
 
1847
1943
  // src/dep-tracker.ts
1848
- import { existsSync as existsSync7 } from "fs";
1849
- import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
1850
- import path9 from "path";
1944
+ import { existsSync as existsSync9 } from "fs";
1945
+ import { readFile as readFile9, writeFile as writeFile6, mkdir as mkdir6 } from "fs/promises";
1946
+ import path11 from "path";
1851
1947
  function parsePackageJson(content) {
1852
1948
  try {
1853
1949
  const pkg = JSON.parse(content);
@@ -1919,7 +2015,7 @@ var KNOWN_MANIFESTS = [
1919
2015
  { name: "pom.xml", parser: parsePomXml }
1920
2016
  ];
1921
2017
  function getParser(file) {
1922
- const base = path9.basename(file);
2018
+ const base = path11.basename(file);
1923
2019
  return KNOWN_MANIFESTS.find((m) => m.name === base)?.parser ?? null;
1924
2020
  }
1925
2021
  function extractMajor(version) {
@@ -1937,32 +2033,32 @@ function isMajorBump(from, to) {
1937
2033
  }
1938
2034
  function resolveManifestFiles(projectRoot, configuredFiles) {
1939
2035
  if (configuredFiles !== void 0) {
1940
- return configuredFiles.map((f) => path9.resolve(projectRoot, f)).filter(existsSync7);
2036
+ return configuredFiles.map((f) => path11.resolve(projectRoot, f)).filter(existsSync9);
1941
2037
  }
1942
- return KNOWN_MANIFESTS.map(({ name }) => path9.join(projectRoot, name)).filter(existsSync7);
2038
+ return KNOWN_MANIFESTS.map(({ name }) => path11.join(projectRoot, name)).filter(existsSync9);
1943
2039
  }
1944
2040
  async function trackDependencies(projectRoot, haiveDir, manifestFiles) {
1945
- const contractsDir = path9.join(haiveDir, "contracts");
1946
- await mkdir4(contractsDir, { recursive: true });
2041
+ const contractsDir = path11.join(haiveDir, "contracts");
2042
+ await mkdir6(contractsDir, { recursive: true });
1947
2043
  const results = [];
1948
2044
  for (const manifestPath of manifestFiles) {
1949
2045
  const parser = getParser(manifestPath);
1950
2046
  if (!parser) continue;
1951
- const content = await readFile7(manifestPath, "utf8");
2047
+ const content = await readFile9(manifestPath, "utf8");
1952
2048
  const currentDeps = parser(content);
1953
- const lockName = `deps-${path9.basename(manifestPath)}.lock`;
1954
- const lockPath = path9.join(contractsDir, lockName);
1955
- if (!existsSync7(lockPath)) {
2049
+ const lockName = `deps-${path11.basename(manifestPath)}.lock`;
2050
+ const lockPath = path11.join(contractsDir, lockName);
2051
+ if (!existsSync9(lockPath)) {
1956
2052
  const snapshot2 = {
1957
- file: path9.relative(projectRoot, manifestPath),
1958
- format: path9.basename(manifestPath),
2053
+ file: path11.relative(projectRoot, manifestPath),
2054
+ format: path11.basename(manifestPath),
1959
2055
  captured_at: (/* @__PURE__ */ new Date()).toISOString(),
1960
2056
  deps: currentDeps
1961
2057
  };
1962
- await writeFile5(lockPath, JSON.stringify(snapshot2, null, 2) + "\n", "utf8");
2058
+ await writeFile6(lockPath, JSON.stringify(snapshot2, null, 2) + "\n", "utf8");
1963
2059
  continue;
1964
2060
  }
1965
- const snapshot = JSON.parse(await readFile7(lockPath, "utf8"));
2061
+ const snapshot = JSON.parse(await readFile9(lockPath, "utf8"));
1966
2062
  const changes = [];
1967
2063
  for (const [name, currentVer] of Object.entries(currentDeps)) {
1968
2064
  const prevVer = snapshot.deps[name];
@@ -1976,22 +2072,22 @@ async function trackDependencies(projectRoot, haiveDir, manifestFiles) {
1976
2072
  }
1977
2073
  }
1978
2074
  if (changes.length > 0) {
1979
- results.push({ file: path9.relative(projectRoot, manifestPath), changes });
2075
+ results.push({ file: path11.relative(projectRoot, manifestPath), changes });
1980
2076
  const updated = {
1981
2077
  ...snapshot,
1982
2078
  captured_at: (/* @__PURE__ */ new Date()).toISOString(),
1983
2079
  deps: currentDeps
1984
2080
  };
1985
- await writeFile5(lockPath, JSON.stringify(updated, null, 2) + "\n", "utf8");
2081
+ await writeFile6(lockPath, JSON.stringify(updated, null, 2) + "\n", "utf8");
1986
2082
  }
1987
2083
  }
1988
2084
  return results;
1989
2085
  }
1990
2086
 
1991
2087
  // src/contract-watcher.ts
1992
- import { existsSync as existsSync8 } from "fs";
1993
- import { readFile as readFile8, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
1994
- import path10 from "path";
2088
+ import { existsSync as existsSync10 } from "fs";
2089
+ import { readFile as readFile10, writeFile as writeFile7, mkdir as mkdir7 } from "fs/promises";
2090
+ import path12 from "path";
1995
2091
  import crypto from "crypto";
1996
2092
  function sha256(content) {
1997
2093
  return crypto.createHash("sha256").update(content).digest("hex");
@@ -2194,14 +2290,14 @@ function diffSnapshots(before, after) {
2194
2290
  return changes;
2195
2291
  }
2196
2292
  function contractLockPath(haiveDir, name) {
2197
- return path10.join(haiveDir, "contracts", `${name}.lock`);
2293
+ return path12.join(haiveDir, "contracts", `${name}.lock`);
2198
2294
  }
2199
2295
  async function snapshotContract(projectRoot, haiveDir, contract) {
2200
- const filePath = path10.resolve(projectRoot, contract.path);
2201
- if (!existsSync8(filePath)) {
2296
+ const filePath = path12.resolve(projectRoot, contract.path);
2297
+ if (!existsSync10(filePath)) {
2202
2298
  throw new Error(`Contract file not found: ${filePath}`);
2203
2299
  }
2204
- const content = await readFile8(filePath, "utf8");
2300
+ const content = await readFile10(filePath, "utf8");
2205
2301
  const parsed = parseByFormat(content, contract.format, filePath);
2206
2302
  const snapshot = {
2207
2303
  name: contract.name,
@@ -2211,23 +2307,23 @@ async function snapshotContract(projectRoot, haiveDir, contract) {
2211
2307
  hash: sha256(content),
2212
2308
  ...parsed
2213
2309
  };
2214
- const contractsDir = path10.join(haiveDir, "contracts");
2215
- await mkdir5(contractsDir, { recursive: true });
2216
- await writeFile6(contractLockPath(haiveDir, contract.name), JSON.stringify(snapshot, null, 2) + "\n", "utf8");
2310
+ const contractsDir = path12.join(haiveDir, "contracts");
2311
+ await mkdir7(contractsDir, { recursive: true });
2312
+ await writeFile7(contractLockPath(haiveDir, contract.name), JSON.stringify(snapshot, null, 2) + "\n", "utf8");
2217
2313
  return snapshot;
2218
2314
  }
2219
2315
  async function diffContract(projectRoot, haiveDir, contract) {
2220
- const filePath = path10.resolve(projectRoot, contract.path);
2221
- if (!existsSync8(filePath)) {
2316
+ const filePath = path12.resolve(projectRoot, contract.path);
2317
+ if (!existsSync10(filePath)) {
2222
2318
  return { contract: contract.name, file: contract.path, changes: [], unchanged: true };
2223
2319
  }
2224
2320
  const lockPath = contractLockPath(haiveDir, contract.name);
2225
- if (!existsSync8(lockPath)) {
2321
+ if (!existsSync10(lockPath)) {
2226
2322
  await snapshotContract(projectRoot, haiveDir, contract);
2227
2323
  return { contract: contract.name, file: contract.path, changes: [], unchanged: true };
2228
2324
  }
2229
- const content = await readFile8(filePath, "utf8");
2230
- const beforeSnapshot = JSON.parse(await readFile8(lockPath, "utf8"));
2325
+ const content = await readFile10(filePath, "utf8");
2326
+ const beforeSnapshot = JSON.parse(await readFile10(lockPath, "utf8"));
2231
2327
  const afterParsed = parseByFormat(content, contract.format, filePath);
2232
2328
  const afterSnapshot = {
2233
2329
  ...beforeSnapshot,
@@ -2237,7 +2333,7 @@ async function diffContract(projectRoot, haiveDir, contract) {
2237
2333
  };
2238
2334
  const changes = diffSnapshots(beforeSnapshot, afterSnapshot);
2239
2335
  if (changes.length > 0) {
2240
- await writeFile6(lockPath, JSON.stringify(afterSnapshot, null, 2) + "\n", "utf8");
2336
+ await writeFile7(lockPath, JSON.stringify(afterSnapshot, null, 2) + "\n", "utf8");
2241
2337
  }
2242
2338
  return {
2243
2339
  contract: contract.name,
@@ -2255,27 +2351,27 @@ async function watchContracts(projectRoot, haiveDir, contractFiles) {
2255
2351
  }
2256
2352
 
2257
2353
  // src/usage-log.ts
2258
- import { appendFile, mkdir as mkdir6, readFile as readFile9, stat as stat2 } from "fs/promises";
2259
- import { existsSync as existsSync9 } from "fs";
2260
- import path11 from "path";
2354
+ import { appendFile as appendFile2, mkdir as mkdir8, readFile as readFile11, stat as stat2 } from "fs/promises";
2355
+ import { existsSync as existsSync11 } from "fs";
2356
+ import path13 from "path";
2261
2357
  var USAGE_LOG_FILE = "tool-usage.jsonl";
2262
2358
  var USAGE_LOG_DIR = ".usage";
2263
2359
  function usageLogPath(paths) {
2264
- return path11.join(paths.haiveDir, USAGE_LOG_DIR, USAGE_LOG_FILE);
2360
+ return path13.join(paths.haiveDir, USAGE_LOG_DIR, USAGE_LOG_FILE);
2265
2361
  }
2266
2362
  async function appendUsageEvent(paths, event) {
2267
2363
  try {
2268
2364
  const file = usageLogPath(paths);
2269
- const dir = path11.dirname(file);
2270
- if (!existsSync9(dir)) await mkdir6(dir, { recursive: true });
2271
- await appendFile(file, JSON.stringify(event) + "\n", "utf8");
2365
+ const dir = path13.dirname(file);
2366
+ if (!existsSync11(dir)) await mkdir8(dir, { recursive: true });
2367
+ await appendFile2(file, JSON.stringify(event) + "\n", "utf8");
2272
2368
  } catch {
2273
2369
  }
2274
2370
  }
2275
2371
  async function readUsageEvents(paths) {
2276
2372
  const file = usageLogPath(paths);
2277
- if (!existsSync9(file)) return [];
2278
- const raw = await readFile9(file, "utf8");
2373
+ if (!existsSync11(file)) return [];
2374
+ const raw = await readFile11(file, "utf8");
2279
2375
  const out = [];
2280
2376
  for (const line of raw.split("\n")) {
2281
2377
  if (!line) continue;
@@ -2324,9 +2420,9 @@ function parseSince(input) {
2324
2420
  }
2325
2421
  async function usageLogSize(paths) {
2326
2422
  const file = usageLogPath(paths);
2327
- if (!existsSync9(file)) return { exists: false, size_bytes: 0, lines: 0 };
2423
+ if (!existsSync11(file)) return { exists: false, size_bytes: 0, lines: 0 };
2328
2424
  const st = await stat2(file);
2329
- const raw = await readFile9(file, "utf8");
2425
+ const raw = await readFile11(file, "utf8");
2330
2426
  return { exists: true, size_bytes: st.size, lines: raw.split("\n").filter((l) => l).length };
2331
2427
  }
2332
2428
 
@@ -2396,21 +2492,21 @@ function extractActionsBriefBody(markdown, maxChars = MAX_DEFAULT_CHARS) {
2396
2492
  }
2397
2493
 
2398
2494
  // src/resolve-project.ts
2399
- import { existsSync as existsSync10 } from "fs";
2400
- import path12 from "path";
2495
+ import { existsSync as existsSync12 } from "fs";
2496
+ import path14 from "path";
2401
2497
  var ROOT_MARKERS2 = [".ai", ".git", "package.json"];
2402
2498
  function markersAtRoot(root) {
2403
2499
  const found = [];
2404
2500
  for (const m of ROOT_MARKERS2) {
2405
- if (existsSync10(path12.join(root, m))) found.push(m);
2501
+ if (existsSync12(path14.join(root, m))) found.push(m);
2406
2502
  }
2407
2503
  return found;
2408
2504
  }
2409
2505
  function resolveProjectInfo(opts = {}) {
2410
2506
  const env = opts.env ?? process.env;
2411
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
2507
+ const cwd = path14.resolve(opts.cwd ?? process.cwd());
2412
2508
  const raw = env.HAIVE_PROJECT_ROOT;
2413
- const explicit = raw !== void 0 && raw !== "" ? path12.resolve(raw) : null;
2509
+ const explicit = raw !== void 0 && raw !== "" ? path14.resolve(raw) : null;
2414
2510
  const resolvedRoot = explicit ?? findProjectRoot(cwd);
2415
2511
  const paths = resolveHaivePaths(resolvedRoot);
2416
2512
  return {
@@ -2418,8 +2514,8 @@ function resolveProjectInfo(opts = {}) {
2418
2514
  resolved_root: resolvedRoot,
2419
2515
  haive_project_root_env: explicit,
2420
2516
  explicit_root: explicit != null,
2421
- haive_dir_exists: existsSync10(paths.haiveDir),
2422
- memories_dir_exists: existsSync10(paths.memoriesDir),
2517
+ haive_dir_exists: existsSync12(paths.haiveDir),
2518
+ memories_dir_exists: existsSync12(paths.memoriesDir),
2423
2519
  runtime_dir: paths.runtimeDir,
2424
2520
  markers_found: markersAtRoot(resolvedRoot)
2425
2521
  };
@@ -2674,16 +2770,16 @@ function findLexicalConflictPairs(memories, opts) {
2674
2770
  }
2675
2771
 
2676
2772
  // src/runtime-journal.ts
2677
- import { mkdir as mkdir7, readFile as readFile10, appendFile as appendFile2 } from "fs/promises";
2678
- import { existsSync as existsSync11 } from "fs";
2679
- import path13 from "path";
2773
+ import { mkdir as mkdir9, readFile as readFile12, appendFile as appendFile3 } from "fs/promises";
2774
+ import { existsSync as existsSync13 } from "fs";
2775
+ import path15 from "path";
2680
2776
  var RUNTIME_JOURNAL_FILENAME = "session-journal.ndjson";
2681
2777
  function runtimeJournalPath(paths) {
2682
- return path13.join(paths.runtimeDir, RUNTIME_JOURNAL_FILENAME);
2778
+ return path15.join(paths.runtimeDir, RUNTIME_JOURNAL_FILENAME);
2683
2779
  }
2684
2780
  async function appendRuntimeJournalEntry(paths, entry) {
2685
2781
  try {
2686
- await mkdir7(paths.runtimeDir, { recursive: true });
2782
+ await mkdir9(paths.runtimeDir, { recursive: true });
2687
2783
  const line = {
2688
2784
  ts: entry.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
2689
2785
  kind: entry.kind,
@@ -2691,7 +2787,7 @@ async function appendRuntimeJournalEntry(paths, entry) {
2691
2787
  ...entry.tool !== void 0 ? { tool: entry.tool } : {},
2692
2788
  ...entry.meta !== void 0 ? { meta: entry.meta } : {}
2693
2789
  };
2694
- await appendFile2(
2790
+ await appendFile3(
2695
2791
  runtimeJournalPath(paths),
2696
2792
  JSON.stringify(line) + "\n",
2697
2793
  "utf8"
@@ -2701,9 +2797,9 @@ async function appendRuntimeJournalEntry(paths, entry) {
2701
2797
  }
2702
2798
  async function readRuntimeJournalTail(paths, limit) {
2703
2799
  const file = runtimeJournalPath(paths);
2704
- if (!existsSync11(file) || limit <= 0) return [];
2800
+ if (!existsSync13(file) || limit <= 0) return [];
2705
2801
  try {
2706
- const raw = await readFile10(file, "utf8");
2802
+ const raw = await readFile12(file, "utf8");
2707
2803
  const lines = raw.trim().split("\n").filter(Boolean);
2708
2804
  const parsed = [];
2709
2805
  for (const line of lines.slice(-limit)) {
@@ -2719,22 +2815,22 @@ async function readRuntimeJournalTail(paths, limit) {
2719
2815
  }
2720
2816
 
2721
2817
  // src/enforcement.ts
2722
- import { mkdir as mkdir8, readdir as readdir4, readFile as readFile11, writeFile as writeFile7 } from "fs/promises";
2723
- import { existsSync as existsSync12 } from "fs";
2724
- import path14 from "path";
2818
+ import { mkdir as mkdir10, readdir as readdir4, readFile as readFile13, writeFile as writeFile8 } from "fs/promises";
2819
+ import { existsSync as existsSync14 } from "fs";
2820
+ import path16 from "path";
2725
2821
  var BRIEFING_MARKER_TTL_MS = 12 * 60 * 60 * 1e3;
2726
2822
  var SESSION_RECAP_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
2727
2823
  function enforcementDir(paths) {
2728
- return path14.join(paths.runtimeDir, "enforcement");
2824
+ return path16.join(paths.runtimeDir, "enforcement");
2729
2825
  }
2730
2826
  function briefingMarkersDir(paths) {
2731
- return path14.join(enforcementDir(paths), "briefings");
2827
+ return path16.join(enforcementDir(paths), "briefings");
2732
2828
  }
2733
2829
  function normalizeSessionId(sessionId) {
2734
2830
  return (sessionId?.trim() || "default").replace(/[^a-zA-Z0-9_.-]+/g, "-").slice(0, 120);
2735
2831
  }
2736
2832
  function briefingMarkerPath(paths, sessionId) {
2737
- return path14.join(briefingMarkersDir(paths), `${normalizeSessionId(sessionId)}.json`);
2833
+ return path16.join(briefingMarkersDir(paths), `${normalizeSessionId(sessionId)}.json`);
2738
2834
  }
2739
2835
  async function writeBriefingMarker(paths, input) {
2740
2836
  const marker = {
@@ -2746,8 +2842,8 @@ async function writeBriefingMarker(paths, input) {
2746
2842
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
2747
2843
  root: paths.root
2748
2844
  };
2749
- await mkdir8(briefingMarkersDir(paths), { recursive: true });
2750
- await writeFile7(
2845
+ await mkdir10(briefingMarkersDir(paths), { recursive: true });
2846
+ await writeFile8(
2751
2847
  briefingMarkerPath(paths, marker.session_id),
2752
2848
  JSON.stringify(marker, null, 2) + "\n",
2753
2849
  "utf8"
@@ -2758,18 +2854,18 @@ async function hasRecentBriefingMarker(paths, sessionId, ttlMs = BRIEFING_MARKER
2758
2854
  const now = Date.now();
2759
2855
  const candidates = [];
2760
2856
  const exact = briefingMarkerPath(paths, sessionId);
2761
- if (existsSync12(exact)) candidates.push(exact);
2857
+ if (existsSync14(exact)) candidates.push(exact);
2762
2858
  try {
2763
2859
  const dir = briefingMarkersDir(paths);
2764
2860
  const files = await readdir4(dir);
2765
2861
  for (const file of files) {
2766
- if (file.endsWith(".json")) candidates.push(path14.join(dir, file));
2862
+ if (file.endsWith(".json")) candidates.push(path16.join(dir, file));
2767
2863
  }
2768
2864
  } catch {
2769
2865
  }
2770
2866
  for (const file of new Set(candidates)) {
2771
2867
  try {
2772
- const marker = JSON.parse(await readFile11(file, "utf8"));
2868
+ const marker = JSON.parse(await readFile13(file, "utf8"));
2773
2869
  const created = Date.parse(marker.created_at);
2774
2870
  if (Number.isFinite(created) && now - created <= ttlMs) return true;
2775
2871
  } catch {
@@ -2781,12 +2877,12 @@ async function readRecentBriefingMarker(paths, sessionId, ttlMs = BRIEFING_MARKE
2781
2877
  const now = Date.now();
2782
2878
  const candidates = [];
2783
2879
  const exact = briefingMarkerPath(paths, sessionId);
2784
- if (existsSync12(exact)) candidates.push(exact);
2880
+ if (existsSync14(exact)) candidates.push(exact);
2785
2881
  try {
2786
2882
  const dir = briefingMarkersDir(paths);
2787
2883
  const files = await readdir4(dir);
2788
2884
  for (const file of files) {
2789
- if (file.endsWith(".json")) candidates.push(path14.join(dir, file));
2885
+ if (file.endsWith(".json")) candidates.push(path16.join(dir, file));
2790
2886
  }
2791
2887
  } catch {
2792
2888
  }
@@ -2794,7 +2890,7 @@ async function readRecentBriefingMarker(paths, sessionId, ttlMs = BRIEFING_MARKE
2794
2890
  let freshestTs = 0;
2795
2891
  for (const file of new Set(candidates)) {
2796
2892
  try {
2797
- const marker = JSON.parse(await readFile11(file, "utf8"));
2893
+ const marker = JSON.parse(await readFile13(file, "utf8"));
2798
2894
  const created = Date.parse(marker.created_at);
2799
2895
  if (!Number.isFinite(created) || now - created > ttlMs) continue;
2800
2896
  if (created > freshestTs) {
@@ -2847,10 +2943,10 @@ function isRetiredMemory(fm, body = "", now = /* @__PURE__ */ new Date()) {
2847
2943
  function normalizeProjectPath(value) {
2848
2944
  return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^[ab]\//, "").replace(/\/+$/g, "");
2849
2945
  }
2850
- function sensorAppliesToPath(sensor, anchorPaths, path15) {
2946
+ function sensorAppliesToPath(sensor, anchorPaths, path17) {
2851
2947
  const scopes = sensor.paths.length > 0 ? sensor.paths : anchorPaths;
2852
2948
  if (scopes.length === 0) return true;
2853
- const target = normalizeProjectPath(path15);
2949
+ const target = normalizeProjectPath(path17);
2854
2950
  return scopes.some((rawScope) => {
2855
2951
  const scope = normalizeProjectPath(rawScope);
2856
2952
  if (!scope) return false;
@@ -3124,8 +3220,8 @@ function normalizeFindingSeverity(raw) {
3124
3220
  return "info";
3125
3221
  }
3126
3222
  }
3127
- function findingKey(tool, ruleId, path15) {
3128
- return `${tool}:${ruleId}:${path15}`;
3223
+ function findingKey(tool, ruleId, path17) {
3224
+ return `${tool}:${ruleId}:${path17}`;
3129
3225
  }
3130
3226
  function coerceJson(input) {
3131
3227
  if (typeof input === "string") {
@@ -3159,8 +3255,8 @@ function parseSarif(input) {
3159
3255
  const physical = asRecord(location.physicalLocation);
3160
3256
  const artifact = asRecord(physical.artifactLocation);
3161
3257
  const region = asRecord(physical.region);
3162
- const path15 = typeof artifact.uri === "string" ? normalizeUri(artifact.uri) : "";
3163
- if (!path15) continue;
3258
+ const path17 = typeof artifact.uri === "string" ? normalizeUri(artifact.uri) : "";
3259
+ if (!path17) continue;
3164
3260
  const line = typeof region.startLine === "number" ? region.startLine : void 0;
3165
3261
  const snippet = typeof asRecord(region.snippet).text === "string" ? asRecord(region.snippet).text.trim() : void 0;
3166
3262
  findings.push({
@@ -3168,10 +3264,10 @@ function parseSarif(input) {
3168
3264
  ruleId,
3169
3265
  message: message.trim(),
3170
3266
  severity,
3171
- path: path15,
3267
+ path: path17,
3172
3268
  ...line !== void 0 ? { line } : {},
3173
3269
  ...snippet ? { snippet } : {},
3174
- key: findingKey(tool, ruleId, path15)
3270
+ key: findingKey(tool, ruleId, path17)
3175
3271
  });
3176
3272
  }
3177
3273
  }
@@ -3190,17 +3286,17 @@ function parseSonar(input) {
3190
3286
  (typeof issue.severity === "string" ? issue.severity : void 0) ?? impactSeverity
3191
3287
  );
3192
3288
  const component = typeof issue.component === "string" ? issue.component : "";
3193
- const path15 = componentToPath(component);
3194
- if (!path15) continue;
3289
+ const path17 = componentToPath(component);
3290
+ if (!path17) continue;
3195
3291
  const line = typeof issue.line === "number" ? issue.line : void 0;
3196
3292
  findings.push({
3197
3293
  tool: "sonar",
3198
3294
  ruleId,
3199
3295
  message,
3200
3296
  severity,
3201
- path: path15,
3297
+ path: path17,
3202
3298
  ...line !== void 0 ? { line } : {},
3203
- key: findingKey("sonar", ruleId, path15)
3299
+ key: findingKey("sonar", ruleId, path17)
3204
3300
  });
3205
3301
  }
3206
3302
  return findings;
@@ -3278,7 +3374,7 @@ function filterNewDrafts(drafts, existingTopics) {
3278
3374
  }
3279
3375
 
3280
3376
  // src/dashboard.ts
3281
- var MS_PER_DAY3 = 24 * 60 * 60 * 1e3;
3377
+ var MS_PER_DAY4 = 24 * 60 * 60 * 1e3;
3282
3378
  function isAnchorless(fm) {
3283
3379
  if (!["decision", "gotcha", "architecture"].includes(fm.type)) return false;
3284
3380
  if (fm.status !== "validated") return false;
@@ -3372,7 +3468,7 @@ function buildDashboard(memories, usage, options = {}) {
3372
3468
  if (isDecaying(memUsage, fm.created_at)) decaying += 1;
3373
3469
  if (impact.tier === "dormant") {
3374
3470
  const anchor = memUsage.last_read_at ?? fm.created_at;
3375
- const ageDays = Math.floor((now.getTime() - new Date(anchor).getTime()) / MS_PER_DAY3);
3471
+ const ageDays = Math.floor((now.getTime() - new Date(anchor).getTime()) / MS_PER_DAY4);
3376
3472
  dormantRows.push({ id: fm.id, last_read_at: memUsage.last_read_at, age_days: ageDays });
3377
3473
  }
3378
3474
  }
@@ -3411,7 +3507,12 @@ function buildDashboard(memories, usage, options = {}) {
3411
3507
  prevention: {
3412
3508
  total_events: preventionEvents,
3413
3509
  memories_with_catches: preventionRows.length,
3414
- top: preventionRows.sort((a, b) => b.prevented_count - a.prevented_count).slice(0, top)
3510
+ top: preventionRows.sort((a, b) => b.prevented_count - a.prevented_count).slice(0, top),
3511
+ trend: computePreventionTrend(options.preventionEvents ?? [], now),
3512
+ recurrence: {
3513
+ ...computeRecurrence(options.preventionEvents ?? []),
3514
+ top: computeRecurrence(options.preventionEvents ?? []).top.slice(0, top)
3515
+ }
3415
3516
  },
3416
3517
  corpus: {
3417
3518
  memory_files: inventory.total,
@@ -3446,6 +3547,7 @@ export {
3446
3547
  MemoryTypeSchema,
3447
3548
  PREVENTION_DEBOUNCE_MS,
3448
3549
  PROJECT_CONTEXT_FILE,
3550
+ PROJECT_CONTEXT_THROTTLE_MS,
3449
3551
  RUNTIME_JOURNAL_FILENAME,
3450
3552
  SESSION_RECAP_TTL_MS,
3451
3553
  STACK_PACK_TAG,
@@ -3459,6 +3561,7 @@ export {
3459
3561
  aggregateUsage,
3460
3562
  allocateBudget,
3461
3563
  antiPatternGateParams,
3564
+ appendPreventionEvent,
3462
3565
  appendRuntimeJournalEntry,
3463
3566
  appendUsageEvent,
3464
3567
  briefingMarkerPath,
@@ -3475,6 +3578,8 @@ export {
3475
3578
  compareImpact,
3476
3579
  compileRegexSensor,
3477
3580
  computeImpact,
3581
+ computePreventionTrend,
3582
+ computeRecurrence,
3478
3583
  configPath,
3479
3584
  contractLockPath,
3480
3585
  deriveConfidence,
@@ -3499,6 +3604,7 @@ export {
3499
3604
  getUsage,
3500
3605
  globToRegExp,
3501
3606
  hasRecentBriefingMarker,
3607
+ hashProjectContext,
3502
3608
  inferModulesFromPaths,
3503
3609
  isAutoPromoteEligible,
3504
3610
  isDecaying,
@@ -3518,6 +3624,7 @@ export {
3518
3624
  loadConfigSync,
3519
3625
  loadMemoriesFromDir,
3520
3626
  loadMemory,
3627
+ loadPreventionEvents,
3521
3628
  loadUsageIndex,
3522
3629
  memoryFilePath,
3523
3630
  memoryMatchesAnchorPaths,
@@ -3532,6 +3639,8 @@ export {
3532
3639
  parseSonar,
3533
3640
  pathsOverlap,
3534
3641
  pickSnippetNeedle,
3642
+ preventionLogPath,
3643
+ projectContextRecentlyEmitted,
3535
3644
  pullCrossRepoSources,
3536
3645
  queryCodeMap,
3537
3646
  rankMemoriesLexical,
@@ -3540,6 +3649,7 @@ export {
3540
3649
  readUsageEvents,
3541
3650
  recordApplied,
3542
3651
  recordPrevention,
3652
+ recordProjectContextEmission,
3543
3653
  recordRejection,
3544
3654
  relPathFrom,
3545
3655
  resolveBriefingBudget,