@hiveai/core 0.3.3 → 0.4.1

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
@@ -1,6 +1,6 @@
1
1
  // src/schema.ts
2
2
  import { z } from "zod";
3
- var MemoryScopeSchema = z.enum(["personal", "team", "module"]);
3
+ var MemoryScopeSchema = z.enum(["personal", "team", "module", "shared"]);
4
4
  var MemoryStatusSchema = z.enum([
5
5
  "draft",
6
6
  "proposed",
@@ -44,12 +44,29 @@ var MemoryFrontmatterSchema = z.object({
44
44
  last_read_at: z.string().nullable().default(null),
45
45
  topic: z.string().optional(),
46
46
  // stable key for upsert — same topic in same scope → update instead of create
47
- revision_count: z.number().int().min(0).default(0)
47
+ revision_count: z.number().int().min(0).default(0),
48
48
  // incremented each time a topic upsert occurs
49
+ /**
50
+ * When true, the AI MUST NOT act on this memory autonomously.
51
+ * It must surface the information to the human developer and wait
52
+ * for explicit confirmation before modifying any code.
53
+ * Used for cross-repo breaking changes, dependency bumps, contract diffs.
54
+ */
55
+ requires_human_approval: z.boolean().default(false)
49
56
  }).refine(
50
57
  (data) => data.scope !== "module" || !!data.module,
51
58
  { message: "module name is required when scope is 'module'", path: ["module"] }
52
59
  );
60
+ var CrossRepoProvenanceSchema = z.object({
61
+ source_name: z.string(),
62
+ // the crossRepoSources name from haive.config.json
63
+ source_path: z.string(),
64
+ // original file path in the source repo
65
+ source_id: z.string(),
66
+ // original memory id
67
+ imported_at: z.string()
68
+ // ISO timestamp of import
69
+ }).optional();
53
70
 
54
71
  // src/parser.ts
55
72
  import matter from "gray-matter";
@@ -108,7 +125,8 @@ function buildFrontmatter(input) {
108
125
  created_at: now.toISOString(),
109
126
  expires_when: null,
110
127
  topic: input.topic,
111
- revision_count: 0
128
+ revision_count: 0,
129
+ related_ids: input.relatedIds ?? []
112
130
  });
113
131
  }
114
132
 
@@ -947,12 +965,585 @@ async function loadConfig(paths) {
947
965
  async function saveConfig(paths, config) {
948
966
  await writeFile3(configPath(paths), JSON.stringify(config, null, 2) + "\n", "utf8");
949
967
  }
968
+
969
+ // src/cross-repo.ts
970
+ import { existsSync as existsSync6 } from "fs";
971
+ import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
972
+ import path8 from "path";
973
+ import { spawnSync } from "child_process";
974
+ async function loadImportMap(cacheDir) {
975
+ const mapPath = path8.join(cacheDir, "import-map.json");
976
+ if (!existsSync6(mapPath)) return {};
977
+ try {
978
+ return JSON.parse(await readFile6(mapPath, "utf8"));
979
+ } catch {
980
+ return {};
981
+ }
982
+ }
983
+ async function saveImportMap(cacheDir, map) {
984
+ await writeFile4(path8.join(cacheDir, "import-map.json"), JSON.stringify(map, null, 2) + "\n", "utf8");
985
+ }
986
+ async function pullCrossRepoSources(paths, config, projectRoot) {
987
+ const sources = config.crossRepoSources ?? [];
988
+ if (sources.length === 0) return [];
989
+ const reports = [];
990
+ for (const source of sources) {
991
+ reports.push(await pullFromSource(paths, source, projectRoot));
992
+ }
993
+ return reports;
994
+ }
995
+ async function pullFromSource(paths, source, projectRoot) {
996
+ const report = {
997
+ source: source.name,
998
+ imported: [],
999
+ updated: [],
1000
+ skipped: [],
1001
+ errors: []
1002
+ };
1003
+ let sourceRoot = null;
1004
+ if (source.path) {
1005
+ const resolved = path8.resolve(projectRoot, source.path);
1006
+ if (!existsSync6(resolved)) {
1007
+ report.errors.push(`Path not found: ${resolved}`);
1008
+ return report;
1009
+ }
1010
+ sourceRoot = resolved;
1011
+ } else if (source.git) {
1012
+ sourceRoot = await cloneOrFetchGitSource(source, paths, report);
1013
+ if (!sourceRoot) return report;
1014
+ } else {
1015
+ report.errors.push(`Source "${source.name}" has neither path nor git \u2014 skipping.`);
1016
+ return report;
1017
+ }
1018
+ const sourcePaths = resolveHaivePaths(sourceRoot);
1019
+ if (!existsSync6(sourcePaths.memoriesDir)) {
1020
+ report.errors.push(`No .ai/memories/ found at ${sourceRoot}`);
1021
+ return report;
1022
+ }
1023
+ const sourceMemories = (await loadMemoriesFromDir(sourcePaths.memoriesDir)).filter(
1024
+ ({ memory }) => {
1025
+ const fm = memory.frontmatter;
1026
+ if (fm.scope !== "shared") return false;
1027
+ if (fm.status === "rejected" || fm.status === "deprecated") return false;
1028
+ if (source.filter?.tags && source.filter.tags.length > 0) {
1029
+ const hasTag = source.filter.tags.some((t) => fm.tags.includes(t));
1030
+ if (!hasTag) return false;
1031
+ }
1032
+ if (source.filter?.types && source.filter.types.length > 0) {
1033
+ if (!source.filter.types.includes(fm.type)) return false;
1034
+ }
1035
+ return true;
1036
+ }
1037
+ );
1038
+ if (sourceMemories.length === 0) {
1039
+ report.skipped.push("no shared memories found in source");
1040
+ return report;
1041
+ }
1042
+ const destDir = path8.join(paths.memoriesDir, "shared", source.name);
1043
+ await mkdir3(destDir, { recursive: true });
1044
+ const cacheDir = path8.join(paths.haiveDir, ".cache", "cross-repo", source.name);
1045
+ await mkdir3(cacheDir, { recursive: true });
1046
+ const importMap = await loadImportMap(cacheDir);
1047
+ const mapDirty = false;
1048
+ let dirty = mapDirty;
1049
+ for (const { memory } of sourceMemories) {
1050
+ const fm = memory.frontmatter;
1051
+ const sourceId = fm.id;
1052
+ const importTag = `cross-repo:${source.name}`;
1053
+ const tags = [.../* @__PURE__ */ new Set([...fm.tags, importTag])];
1054
+ const importedBodyPrefix = `> **Imported from \`${source.name}\`** (original id: \`${sourceId}\`)
1055
+ > Imported at: ${(/* @__PURE__ */ new Date()).toISOString()}
1056
+
1057
+ `;
1058
+ const existingLocalPath = importMap[sourceId];
1059
+ if (existingLocalPath && existsSync6(existingLocalPath)) {
1060
+ const existingFiles = await loadMemoriesFromDir(destDir);
1061
+ const existingEntry = existingFiles.find(({ filePath }) => filePath === existingLocalPath);
1062
+ const sourceBodyStripped = memory.body.trim();
1063
+ const existingBodyStripped = (existingEntry?.memory.body ?? "").replace(/^>.*\n>.*\n\n/m, "").trim();
1064
+ if (existingBodyStripped === sourceBodyStripped) {
1065
+ report.skipped.push(sourceId);
1066
+ continue;
1067
+ }
1068
+ const updatedBody = importedBodyPrefix + memory.body;
1069
+ if (existingEntry) {
1070
+ await writeFile4(
1071
+ existingLocalPath,
1072
+ serializeMemory({ frontmatter: existingEntry.memory.frontmatter, body: updatedBody }),
1073
+ "utf8"
1074
+ );
1075
+ }
1076
+ report.updated.push(sourceId);
1077
+ } else {
1078
+ const slug = `${source.name}-${fm.id.slice(0, 40)}`;
1079
+ const newFm = buildFrontmatter({
1080
+ type: fm.type,
1081
+ slug,
1082
+ scope: "team",
1083
+ module: void 0,
1084
+ status: "validated",
1085
+ tags,
1086
+ domain: fm.domain,
1087
+ author: fm.author,
1088
+ paths: fm.anchor.paths,
1089
+ symbols: fm.anchor.symbols,
1090
+ commit: fm.anchor.commit,
1091
+ topic: fm.topic ? `${source.name}:${fm.topic}` : void 0
1092
+ });
1093
+ const body = importedBodyPrefix + memory.body;
1094
+ const destPath = path8.join(destDir, `${newFm.id}.md`);
1095
+ await writeFile4(destPath, serializeMemory({ frontmatter: newFm, body }), "utf8");
1096
+ importMap[sourceId] = destPath;
1097
+ dirty = true;
1098
+ report.imported.push(sourceId);
1099
+ }
1100
+ }
1101
+ if (dirty) await saveImportMap(cacheDir, importMap);
1102
+ return report;
1103
+ }
1104
+ async function cloneOrFetchGitSource(source, paths, report) {
1105
+ const cacheDir = path8.join(paths.haiveDir, ".cache", "cross-repo", source.name);
1106
+ await mkdir3(cacheDir, { recursive: true });
1107
+ if (existsSync6(path8.join(cacheDir, ".git"))) {
1108
+ const result = spawnSync("git", ["fetch", "--depth=1", "origin"], {
1109
+ cwd: cacheDir,
1110
+ encoding: "utf8"
1111
+ });
1112
+ if (result.status !== 0) {
1113
+ report.errors.push(`git fetch failed for ${source.name}: ${result.stderr}`);
1114
+ return null;
1115
+ }
1116
+ spawnSync("git", ["reset", "--hard", "FETCH_HEAD"], { cwd: cacheDir });
1117
+ } else {
1118
+ const result = spawnSync(
1119
+ "git",
1120
+ ["clone", "--depth=1", source.git, "."],
1121
+ { cwd: cacheDir, encoding: "utf8" }
1122
+ );
1123
+ if (result.status !== 0) {
1124
+ report.errors.push(`git clone failed for ${source.name}: ${result.stderr}`);
1125
+ return null;
1126
+ }
1127
+ }
1128
+ return cacheDir;
1129
+ }
1130
+
1131
+ // src/dep-tracker.ts
1132
+ import { existsSync as existsSync7 } from "fs";
1133
+ import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
1134
+ import path9 from "path";
1135
+ function parsePackageJson(content) {
1136
+ try {
1137
+ const pkg = JSON.parse(content);
1138
+ return {
1139
+ ...pkg.dependencies,
1140
+ ...pkg.devDependencies,
1141
+ ...pkg.peerDependencies
1142
+ };
1143
+ } catch {
1144
+ return {};
1145
+ }
1146
+ }
1147
+ function parseGoMod(content) {
1148
+ const result = {};
1149
+ for (const line of content.split("\n")) {
1150
+ const m = line.trim().match(/^(\S+)\s+(v[\d.]+)/);
1151
+ if (m?.[1] && m[2]) result[m[1]] = m[2];
1152
+ }
1153
+ return result;
1154
+ }
1155
+ function parseRequirementsTxt(content) {
1156
+ const result = {};
1157
+ for (const line of content.split("\n")) {
1158
+ const clean = (line.split("#")[0] ?? "").trim();
1159
+ const m = clean.match(/^([A-Za-z0-9_.-]+)[=><~!]+(.+)$/);
1160
+ if (m?.[1] && m[2]) result[m[1].toLowerCase()] = m[2].trim();
1161
+ }
1162
+ return result;
1163
+ }
1164
+ function parseCargotoml(content) {
1165
+ const result = {};
1166
+ let inDeps = false;
1167
+ for (const line of content.split("\n")) {
1168
+ if (/^\[(dependencies|dev-dependencies|build-dependencies)\]/.test(line.trim())) {
1169
+ inDeps = true;
1170
+ continue;
1171
+ }
1172
+ if (line.startsWith("[") && !line.includes("dependencies")) {
1173
+ inDeps = false;
1174
+ continue;
1175
+ }
1176
+ if (!inDeps) continue;
1177
+ const simple = line.match(/^(\w[\w-]*)\s*=\s*"([^"]+)"/);
1178
+ if (simple?.[1] && simple[2]) {
1179
+ result[simple[1]] = simple[2];
1180
+ continue;
1181
+ }
1182
+ const table = line.match(/^(\w[\w-]*)\s*=\s*\{[^}]*version\s*=\s*"([^"]+)"/);
1183
+ if (table?.[1] && table[2]) result[table[1]] = table[2];
1184
+ }
1185
+ return result;
1186
+ }
1187
+ function parsePomXml(content) {
1188
+ const result = {};
1189
+ const depRe = /<dependency>[\s\S]*?<groupId>([^<]+)<\/groupId>[\s\S]*?<artifactId>([^<]+)<\/artifactId>[\s\S]*?<version>([^<]+)<\/version>[\s\S]*?<\/dependency>/g;
1190
+ let m;
1191
+ while ((m = depRe.exec(content)) !== null) {
1192
+ if (m[1] && m[2] && m[3]) {
1193
+ result[`${m[1].trim()}:${m[2].trim()}`] = m[3].trim();
1194
+ }
1195
+ }
1196
+ return result;
1197
+ }
1198
+ var KNOWN_MANIFESTS = [
1199
+ { name: "package.json", parser: parsePackageJson },
1200
+ { name: "go.mod", parser: parseGoMod },
1201
+ { name: "requirements.txt", parser: parseRequirementsTxt },
1202
+ { name: "Cargo.toml", parser: parseCargotoml },
1203
+ { name: "pom.xml", parser: parsePomXml }
1204
+ ];
1205
+ function getParser(file) {
1206
+ const base = path9.basename(file);
1207
+ return KNOWN_MANIFESTS.find((m) => m.name === base)?.parser ?? null;
1208
+ }
1209
+ function extractMajor(version) {
1210
+ const clean = version.replace(/^[^0-9]*/, "");
1211
+ const firstPart = clean.split(".")[0];
1212
+ if (!firstPart) return null;
1213
+ const n = parseInt(firstPart, 10);
1214
+ return isNaN(n) ? null : n;
1215
+ }
1216
+ function isMajorBump(from, to) {
1217
+ const fromMajor = extractMajor(from);
1218
+ const toMajor = extractMajor(to);
1219
+ if (fromMajor === null || toMajor === null) return false;
1220
+ return toMajor > fromMajor;
1221
+ }
1222
+ function resolveManifestFiles(projectRoot, configuredFiles) {
1223
+ if (configuredFiles !== void 0) {
1224
+ return configuredFiles.map((f) => path9.resolve(projectRoot, f)).filter(existsSync7);
1225
+ }
1226
+ return KNOWN_MANIFESTS.map(({ name }) => path9.join(projectRoot, name)).filter(existsSync7);
1227
+ }
1228
+ async function trackDependencies(projectRoot, haiveDir, manifestFiles) {
1229
+ const contractsDir = path9.join(haiveDir, "contracts");
1230
+ await mkdir4(contractsDir, { recursive: true });
1231
+ const results = [];
1232
+ for (const manifestPath of manifestFiles) {
1233
+ const parser = getParser(manifestPath);
1234
+ if (!parser) continue;
1235
+ const content = await readFile7(manifestPath, "utf8");
1236
+ const currentDeps = parser(content);
1237
+ const lockName = `deps-${path9.basename(manifestPath)}.lock`;
1238
+ const lockPath = path9.join(contractsDir, lockName);
1239
+ if (!existsSync7(lockPath)) {
1240
+ const snapshot2 = {
1241
+ file: path9.relative(projectRoot, manifestPath),
1242
+ format: path9.basename(manifestPath),
1243
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
1244
+ deps: currentDeps
1245
+ };
1246
+ await writeFile5(lockPath, JSON.stringify(snapshot2, null, 2) + "\n", "utf8");
1247
+ continue;
1248
+ }
1249
+ const snapshot = JSON.parse(await readFile7(lockPath, "utf8"));
1250
+ const changes = [];
1251
+ for (const [name, currentVer] of Object.entries(currentDeps)) {
1252
+ const prevVer = snapshot.deps[name];
1253
+ if (prevVer && prevVer !== currentVer) {
1254
+ changes.push({
1255
+ name,
1256
+ from: prevVer,
1257
+ to: currentVer,
1258
+ isMajorBump: isMajorBump(prevVer, currentVer)
1259
+ });
1260
+ }
1261
+ }
1262
+ if (changes.length > 0) {
1263
+ results.push({ file: path9.relative(projectRoot, manifestPath), changes });
1264
+ const updated = {
1265
+ ...snapshot,
1266
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
1267
+ deps: currentDeps
1268
+ };
1269
+ await writeFile5(lockPath, JSON.stringify(updated, null, 2) + "\n", "utf8");
1270
+ }
1271
+ }
1272
+ return results;
1273
+ }
1274
+
1275
+ // src/contract-watcher.ts
1276
+ import { existsSync as existsSync8 } from "fs";
1277
+ import { readFile as readFile8, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
1278
+ import path10 from "path";
1279
+ import crypto from "crypto";
1280
+ function sha256(content) {
1281
+ return crypto.createHash("sha256").update(content).digest("hex");
1282
+ }
1283
+ function parseOpenApi(content, file) {
1284
+ try {
1285
+ let doc;
1286
+ if (file.endsWith(".yaml") || file.endsWith(".yml")) {
1287
+ doc = parseYamlPaths(content);
1288
+ } else {
1289
+ doc = JSON.parse(content);
1290
+ }
1291
+ const paths = doc.paths ?? {};
1292
+ const endpoints = [];
1293
+ for (const [routePath, methods] of Object.entries(paths)) {
1294
+ for (const method of Object.keys(methods)) {
1295
+ if (["get", "post", "put", "patch", "delete", "head", "options"].includes(method)) {
1296
+ endpoints.push(`${method.toUpperCase()} ${routePath}`);
1297
+ }
1298
+ }
1299
+ }
1300
+ const schemas = doc.components?.schemas ?? doc.definitions ?? {};
1301
+ const types = Object.keys(schemas);
1302
+ const fields = {};
1303
+ for (const [typeName, schema] of Object.entries(schemas)) {
1304
+ const props = schema.properties ?? {};
1305
+ fields[typeName] = Object.keys(props);
1306
+ }
1307
+ return { endpoints, types, fields };
1308
+ } catch {
1309
+ return {};
1310
+ }
1311
+ }
1312
+ function parseYamlPaths(content) {
1313
+ const result = {};
1314
+ let currentPath = "";
1315
+ for (const line of content.split("\n")) {
1316
+ const pathMatch = line.match(/^ (\/[^\s:]+):/);
1317
+ if (pathMatch?.[1]) {
1318
+ currentPath = pathMatch[1];
1319
+ result[currentPath] = {};
1320
+ continue;
1321
+ }
1322
+ if (currentPath) {
1323
+ const methodMatch = line.match(/^ (get|post|put|patch|delete|head|options):/);
1324
+ if (methodMatch?.[1]) (result[currentPath] ??= {})[methodMatch[1]] = true;
1325
+ }
1326
+ }
1327
+ return { paths: result };
1328
+ }
1329
+ function parseGraphQL(content) {
1330
+ const types = [];
1331
+ const fields = {};
1332
+ let currentType = "";
1333
+ for (const line of content.split("\n")) {
1334
+ const typeMatch = line.match(/^(?:type|interface|union|enum|input)\s+(\w+)/);
1335
+ if (typeMatch?.[1]) {
1336
+ currentType = typeMatch[1];
1337
+ types.push(currentType);
1338
+ fields[currentType] = [];
1339
+ continue;
1340
+ }
1341
+ if (currentType && line.trim().startsWith("}")) {
1342
+ currentType = "";
1343
+ continue;
1344
+ }
1345
+ if (currentType) {
1346
+ const fieldMatch = line.match(/^\s+(\w+)\s*[:(]/);
1347
+ if (fieldMatch?.[1]) (fields[currentType] ??= []).push(fieldMatch[1]);
1348
+ }
1349
+ }
1350
+ return { types, fields };
1351
+ }
1352
+ function parseProto(content) {
1353
+ const types = [];
1354
+ const fields = {};
1355
+ let currentMsg = "";
1356
+ for (const line of content.split("\n")) {
1357
+ const msgMatch = line.match(/^(?:message|service|enum)\s+(\w+)/);
1358
+ if (msgMatch?.[1]) {
1359
+ currentMsg = msgMatch[1];
1360
+ types.push(currentMsg);
1361
+ fields[currentMsg] = [];
1362
+ continue;
1363
+ }
1364
+ if (currentMsg && line.trim() === "}") {
1365
+ currentMsg = "";
1366
+ continue;
1367
+ }
1368
+ if (currentMsg) {
1369
+ const fieldMatch = line.match(/^\s+(?:optional|required|repeated)?\s*\w+\s+(\w+)\s*=/);
1370
+ if (fieldMatch?.[1]) (fields[currentMsg] ??= []).push(fieldMatch[1]);
1371
+ const rpcMatch = line.match(/^\s+rpc\s+(\w+)/);
1372
+ if (rpcMatch?.[1]) (fields[currentMsg] ??= []).push(`rpc:${rpcMatch[1]}`);
1373
+ }
1374
+ }
1375
+ return { types, fields };
1376
+ }
1377
+ function parseTypescript(content) {
1378
+ const types = [];
1379
+ const fields = {};
1380
+ let currentType = "";
1381
+ let braceDepth = 0;
1382
+ for (const line of content.split("\n")) {
1383
+ const exportMatch = line.match(/^export\s+(?:declare\s+)?(?:interface|class|type|enum)\s+(\w+)/);
1384
+ if (exportMatch?.[1]) {
1385
+ currentType = exportMatch[1];
1386
+ types.push(currentType);
1387
+ fields[currentType] = [];
1388
+ }
1389
+ if (currentType) {
1390
+ braceDepth += (line.match(/{/g) ?? []).length;
1391
+ braceDepth -= (line.match(/}/g) ?? []).length;
1392
+ if (braceDepth <= 0 && line.includes("}")) {
1393
+ currentType = "";
1394
+ braceDepth = 0;
1395
+ continue;
1396
+ }
1397
+ const memberMatch = line.match(/^\s+(?:readonly\s+)?(\w+)\s*[?:(!]/);
1398
+ if (memberMatch?.[1] && currentType) (fields[currentType] ??= []).push(memberMatch[1]);
1399
+ }
1400
+ }
1401
+ return { types, fields };
1402
+ }
1403
+ function parseByFormat(content, format, filePath) {
1404
+ switch (format) {
1405
+ case "openapi":
1406
+ return parseOpenApi(content, filePath);
1407
+ case "graphql":
1408
+ return parseGraphQL(content);
1409
+ case "proto":
1410
+ return parseProto(content);
1411
+ case "typescript":
1412
+ return parseTypescript(content);
1413
+ case "json-schema": {
1414
+ try {
1415
+ const schema = JSON.parse(content);
1416
+ const types = Object.keys(schema.definitions ?? schema.properties ?? {});
1417
+ return { types };
1418
+ } catch {
1419
+ return {};
1420
+ }
1421
+ }
1422
+ default:
1423
+ return {};
1424
+ }
1425
+ }
1426
+ function diffLists(before, after, kind) {
1427
+ const changes = [];
1428
+ const beforeSet = new Set(before);
1429
+ const afterSet = new Set(after);
1430
+ for (const item of beforeSet) {
1431
+ if (!afterSet.has(item)) {
1432
+ const isBreaking = kind === "endpoint" || kind === "field" || kind === "type";
1433
+ changes.push({
1434
+ kind: `${kind}_removed`,
1435
+ description: `${kind} removed: ${item}`,
1436
+ severity: isBreaking ? "breaking" : "unknown"
1437
+ });
1438
+ }
1439
+ }
1440
+ for (const item of afterSet) {
1441
+ if (!beforeSet.has(item)) {
1442
+ changes.push({
1443
+ kind: `${kind}_added`,
1444
+ description: `${kind} added: ${item}`,
1445
+ severity: "additive"
1446
+ });
1447
+ }
1448
+ }
1449
+ return changes;
1450
+ }
1451
+ function diffSnapshots(before, after) {
1452
+ if (before.hash === after.hash) return [];
1453
+ const changes = [];
1454
+ if (before.endpoints && after.endpoints) {
1455
+ changes.push(...diffLists(before.endpoints, after.endpoints, "endpoint"));
1456
+ }
1457
+ if (before.types && after.types) {
1458
+ changes.push(...diffLists(before.types, after.types, "type"));
1459
+ }
1460
+ if (before.fields && after.fields) {
1461
+ const allTypes = /* @__PURE__ */ new Set([...Object.keys(before.fields), ...Object.keys(after.fields)]);
1462
+ for (const typeName of allTypes) {
1463
+ const beforeFields = before.fields[typeName] ?? [];
1464
+ const afterFields = after.fields[typeName] ?? [];
1465
+ const fieldChanges = diffLists(beforeFields, afterFields, "field");
1466
+ for (const fc of fieldChanges) {
1467
+ changes.push({ ...fc, description: `${typeName}.${fc.description}` });
1468
+ }
1469
+ }
1470
+ }
1471
+ if (changes.length === 0) {
1472
+ changes.push({
1473
+ kind: "content_changed",
1474
+ description: "Contract file content changed (no structured diff available)",
1475
+ severity: "unknown"
1476
+ });
1477
+ }
1478
+ return changes;
1479
+ }
1480
+ function contractLockPath(haiveDir, name) {
1481
+ return path10.join(haiveDir, "contracts", `${name}.lock`);
1482
+ }
1483
+ async function snapshotContract(projectRoot, haiveDir, contract) {
1484
+ const filePath = path10.resolve(projectRoot, contract.path);
1485
+ if (!existsSync8(filePath)) {
1486
+ throw new Error(`Contract file not found: ${filePath}`);
1487
+ }
1488
+ const content = await readFile8(filePath, "utf8");
1489
+ const parsed = parseByFormat(content, contract.format, filePath);
1490
+ const snapshot = {
1491
+ name: contract.name,
1492
+ path: contract.path,
1493
+ format: contract.format,
1494
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
1495
+ hash: sha256(content),
1496
+ ...parsed
1497
+ };
1498
+ const contractsDir = path10.join(haiveDir, "contracts");
1499
+ await mkdir5(contractsDir, { recursive: true });
1500
+ await writeFile6(contractLockPath(haiveDir, contract.name), JSON.stringify(snapshot, null, 2) + "\n", "utf8");
1501
+ return snapshot;
1502
+ }
1503
+ async function diffContract(projectRoot, haiveDir, contract) {
1504
+ const filePath = path10.resolve(projectRoot, contract.path);
1505
+ if (!existsSync8(filePath)) {
1506
+ return { contract: contract.name, file: contract.path, changes: [], unchanged: true };
1507
+ }
1508
+ const lockPath = contractLockPath(haiveDir, contract.name);
1509
+ if (!existsSync8(lockPath)) {
1510
+ await snapshotContract(projectRoot, haiveDir, contract);
1511
+ return { contract: contract.name, file: contract.path, changes: [], unchanged: true };
1512
+ }
1513
+ const content = await readFile8(filePath, "utf8");
1514
+ const beforeSnapshot = JSON.parse(await readFile8(lockPath, "utf8"));
1515
+ const afterParsed = parseByFormat(content, contract.format, filePath);
1516
+ const afterSnapshot = {
1517
+ ...beforeSnapshot,
1518
+ hash: sha256(content),
1519
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
1520
+ ...afterParsed
1521
+ };
1522
+ const changes = diffSnapshots(beforeSnapshot, afterSnapshot);
1523
+ if (changes.length > 0) {
1524
+ await writeFile6(lockPath, JSON.stringify(afterSnapshot, null, 2) + "\n", "utf8");
1525
+ }
1526
+ return {
1527
+ contract: contract.name,
1528
+ file: contract.path,
1529
+ changes,
1530
+ unchanged: changes.length === 0
1531
+ };
1532
+ }
1533
+ async function watchContracts(projectRoot, haiveDir, contractFiles) {
1534
+ const results = [];
1535
+ for (const contract of contractFiles) {
1536
+ results.push(await diffContract(projectRoot, haiveDir, contract));
1537
+ }
1538
+ return results.filter((r) => !r.unchanged);
1539
+ }
950
1540
  export {
951
1541
  AUTOPILOT_DEFAULTS,
952
1542
  AnchorSchema,
953
1543
  CHARS_PER_TOKEN,
954
1544
  CODE_MAP_FILE,
955
1545
  CONFIG_FILE,
1546
+ CrossRepoProvenanceSchema,
956
1547
  DECAY_DAYS,
957
1548
  DEFAULT_AUTO_PROMOTE_RULE,
958
1549
  DEFAULT_CONFIDENCE_THRESHOLDS,
@@ -971,7 +1562,9 @@ export {
971
1562
  bumpRead,
972
1563
  codeMapPath,
973
1564
  configPath,
1565
+ contractLockPath,
974
1566
  deriveConfidence,
1567
+ diffContract,
975
1568
  emptyUsage,
976
1569
  emptyUsageIndex,
977
1570
  estimateTokens,
@@ -995,19 +1588,24 @@ export {
995
1588
  parseMemory,
996
1589
  pathsOverlap,
997
1590
  pickSnippetNeedle,
1591
+ pullCrossRepoSources,
998
1592
  queryCodeMap,
999
1593
  recordRejection,
1000
1594
  relPathFrom,
1001
1595
  resolveHaivePaths,
1596
+ resolveManifestFiles,
1002
1597
  saveCodeMap,
1003
1598
  saveConfig,
1004
1599
  saveUsageIndex,
1005
1600
  serializeMemory,
1601
+ snapshotContract,
1006
1602
  stripPrivate,
1007
1603
  tokenizeQuery,
1604
+ trackDependencies,
1008
1605
  trackReads,
1009
1606
  truncateToTokens,
1010
1607
  usagePath,
1011
- verifyAnchor
1608
+ verifyAnchor,
1609
+ watchContracts
1012
1610
  };
1013
1611
  //# sourceMappingURL=index.js.map