@hivelore/core 0.54.0 → 0.57.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.d.ts +341 -1
- package/dist/index.js +558 -189
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1042,6 +1042,26 @@ function renderPreventionReceiptShare(receipt) {
|
|
|
1042
1042
|
lines.push("", `<sub>${HIVELORE_ATTRIBUTION}</sub>`);
|
|
1043
1043
|
return lines.join("\n");
|
|
1044
1044
|
}
|
|
1045
|
+
var PREVENTION_RECEIPT_MARKER = "<!-- haive:prevention-receipt -->";
|
|
1046
|
+
function renderPreventionComment(receipt, findings = []) {
|
|
1047
|
+
const fired = findings.filter((f) => f.code === "sensor-block" || f.code === "sensor-warn");
|
|
1048
|
+
const lines = [PREVENTION_RECEIPT_MARKER, "", "## Hivelore prevention receipt", ""];
|
|
1049
|
+
if (fired.length === 0) {
|
|
1050
|
+
lines.push("No documented sensor fired on this PR.", "");
|
|
1051
|
+
} else {
|
|
1052
|
+
lines.push("### Fired on this PR", "");
|
|
1053
|
+
for (const f of fired) {
|
|
1054
|
+
const where = f.file ? ` \u2014 \`${f.file}\`` : "";
|
|
1055
|
+
lines.push(`- **${f.memory_ids?.[0] ?? "sensor"}**${where} \u2014 ${f.message ?? "sensor fired"}`);
|
|
1056
|
+
if (f.matched_line) lines.push(` \`\`\`
|
|
1057
|
+
${f.matched_line}
|
|
1058
|
+
\`\`\``);
|
|
1059
|
+
}
|
|
1060
|
+
lines.push("");
|
|
1061
|
+
}
|
|
1062
|
+
lines.push(renderPreventionReceiptShare(receipt));
|
|
1063
|
+
return lines.join("\n");
|
|
1064
|
+
}
|
|
1045
1065
|
async function loadPreventionEvents(paths) {
|
|
1046
1066
|
const file = preventionLogPath(paths);
|
|
1047
1067
|
if (!existsSync4(file)) return [];
|
|
@@ -1204,6 +1224,244 @@ async function recordProjectContextEmission(paths, hash, now = Date.now()) {
|
|
|
1204
1224
|
});
|
|
1205
1225
|
}
|
|
1206
1226
|
|
|
1227
|
+
// src/gate-reminder.ts
|
|
1228
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
1229
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1230
|
+
import path8 from "path";
|
|
1231
|
+
var GATE_REMINDER_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
1232
|
+
function reminderMarkerPath(paths) {
|
|
1233
|
+
return path8.join(paths.haiveDir, ".cache", "gate-reminders.json");
|
|
1234
|
+
}
|
|
1235
|
+
async function readMarkers(paths) {
|
|
1236
|
+
const file = reminderMarkerPath(paths);
|
|
1237
|
+
if (!existsSync6(file)) return {};
|
|
1238
|
+
try {
|
|
1239
|
+
const parsed = JSON.parse(await readFile6(file, "utf8"));
|
|
1240
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1241
|
+
} catch {
|
|
1242
|
+
return {};
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
async function shouldExpandGateReminder(paths, key, now = Date.now(), windowMs = GATE_REMINDER_WINDOW_MS) {
|
|
1246
|
+
const last = (await readMarkers(paths))[key];
|
|
1247
|
+
if (!last) return true;
|
|
1248
|
+
const at = Date.parse(last);
|
|
1249
|
+
return !Number.isFinite(at) || now - at >= windowMs;
|
|
1250
|
+
}
|
|
1251
|
+
async function recordGateReminder(paths, key, now = Date.now()) {
|
|
1252
|
+
const file = reminderMarkerPath(paths);
|
|
1253
|
+
const markers = await readMarkers(paths);
|
|
1254
|
+
markers[key] = new Date(now).toISOString();
|
|
1255
|
+
await mkdir4(path8.dirname(file), { recursive: true }).catch(() => {
|
|
1256
|
+
});
|
|
1257
|
+
await writeFile3(file, JSON.stringify(markers, null, 2), "utf8").catch(() => {
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// src/gate-verdict.ts
|
|
1262
|
+
var PROCESS_GATE_CODES = /* @__PURE__ */ new Set([
|
|
1263
|
+
"briefing-missing",
|
|
1264
|
+
"session-recap-missing",
|
|
1265
|
+
"decision-coverage-missing",
|
|
1266
|
+
"bootstrap-incomplete"
|
|
1267
|
+
]);
|
|
1268
|
+
var CONTENT_CATCH_CODES = /* @__PURE__ */ new Set(["sensor-block", "precommit-policy-block"]);
|
|
1269
|
+
var CONTENT_CODES = /* @__PURE__ */ new Set([...CONTENT_CATCH_CODES, "sensor-warn"]);
|
|
1270
|
+
var SETUP_GATE_CODES = /* @__PURE__ */ new Set([
|
|
1271
|
+
...PROCESS_GATE_CODES,
|
|
1272
|
+
"enforcement-score-below-threshold"
|
|
1273
|
+
]);
|
|
1274
|
+
var DEFAULT_POSTURE = "balanced";
|
|
1275
|
+
var POSTURE_DEFAULTS = {
|
|
1276
|
+
// Report everything, refuse nothing. For adopting Hivelore on a repo mid-flight.
|
|
1277
|
+
advisory: { mode: "advisory", processGate: "warn", humanCommits: "relaxed" },
|
|
1278
|
+
// Refuse on deterministic, code-bound evidence only. The default.
|
|
1279
|
+
balanced: { mode: "strict", processGate: "warn", humanCommits: "relaxed" },
|
|
1280
|
+
// Process gates bind too, at the sharing points. For teams that want the workflow enforced.
|
|
1281
|
+
strict: { mode: "strict", processGate: "block", humanCommits: "strict" }
|
|
1282
|
+
};
|
|
1283
|
+
function resolveGatePolicy(input) {
|
|
1284
|
+
const cfg = input ?? {};
|
|
1285
|
+
const posture = cfg.posture ?? DEFAULT_POSTURE;
|
|
1286
|
+
const base = POSTURE_DEFAULTS[posture] ?? POSTURE_DEFAULTS[DEFAULT_POSTURE];
|
|
1287
|
+
const overrides = [];
|
|
1288
|
+
for (const key of ["mode", "processGate", "humanCommits"]) {
|
|
1289
|
+
if (cfg[key] !== void 0) overrides.push(key);
|
|
1290
|
+
}
|
|
1291
|
+
return {
|
|
1292
|
+
posture,
|
|
1293
|
+
mode: cfg.mode ?? base.mode,
|
|
1294
|
+
processGate: cfg.processGate ?? base.processGate,
|
|
1295
|
+
humanCommits: cfg.humanCommits ?? base.humanCommits,
|
|
1296
|
+
scoreThreshold: cfg.scoreThreshold ?? 80,
|
|
1297
|
+
overrides
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
function describePosture(policy) {
|
|
1301
|
+
const base = policy.posture === "advisory" ? "reports everything, refuses nothing" : policy.posture === "strict" ? "refuses on deterministic findings AND on process gates at pre-push/CI" : "refuses on deterministic findings only (sensors, anti-patterns, stale anchors on touched files)";
|
|
1302
|
+
const pinned = policy.overrides.length > 0 ? ` \xB7 overridden: ${policy.overrides.join(", ")}` : "";
|
|
1303
|
+
return `${policy.posture} \u2014 ${base}${pinned}`;
|
|
1304
|
+
}
|
|
1305
|
+
function processGateDecision(policy, stage, isAgent) {
|
|
1306
|
+
if (policy.processGate !== "block") {
|
|
1307
|
+
return {
|
|
1308
|
+
refuses: false,
|
|
1309
|
+
reason: `advisory: process gates report, they do not refuse \u2014 only sensors and other deterministic findings block. Set enforcement.posture="strict" (or processGate="block") to change that.`
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
if (stage === "pre-commit" || stage === "local") {
|
|
1313
|
+
return {
|
|
1314
|
+
refuses: false,
|
|
1315
|
+
reason: "advisory at commit time: process gates bind the sharing points (pre-push, CI), not local iteration."
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
if (stage !== "ci" && !isAgent && policy.humanCommits === "relaxed") {
|
|
1319
|
+
return {
|
|
1320
|
+
refuses: false,
|
|
1321
|
+
reason: `relaxed to a warning: no agent harness detected, so this human commit is not bound by agent process gates \u2014 set enforcement.humanCommits="strict" to change that.`
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
return { refuses: true, reason: "enforced: process gates bind at this sharing point." };
|
|
1325
|
+
}
|
|
1326
|
+
function penaltyOf(finding) {
|
|
1327
|
+
if (finding.severity === "error") return finding.impact ?? 25;
|
|
1328
|
+
if (finding.severity === "warn") return finding.impact ?? 8;
|
|
1329
|
+
return 0;
|
|
1330
|
+
}
|
|
1331
|
+
function computeBaselineHealth(findings, threshold) {
|
|
1332
|
+
const baseline = findings.filter((f) => !CONTENT_CODES.has(f.code));
|
|
1333
|
+
const penalty = baseline.reduce((sum, f) => sum + penaltyOf(f), 0);
|
|
1334
|
+
return {
|
|
1335
|
+
score: Math.max(0, Math.min(100, 100 - penalty)),
|
|
1336
|
+
threshold,
|
|
1337
|
+
checks: {
|
|
1338
|
+
total: findings.length,
|
|
1339
|
+
ok: findings.filter((f) => f.severity === "ok").length,
|
|
1340
|
+
warn: findings.filter((f) => f.severity === "warn").length,
|
|
1341
|
+
error: findings.filter((f) => f.severity === "error").length
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
function dedupeRefusals(findings) {
|
|
1346
|
+
const byMemory = /* @__PURE__ */ new Map();
|
|
1347
|
+
const out = [];
|
|
1348
|
+
for (const finding of findings) {
|
|
1349
|
+
if (!CONTENT_CATCH_CODES.has(finding.code) || finding.severity !== "error") continue;
|
|
1350
|
+
const key = finding.memory_ids?.[0];
|
|
1351
|
+
if (!key) {
|
|
1352
|
+
out.push(finding);
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
const existing = byMemory.get(key);
|
|
1356
|
+
if (!existing || !existing.matched_line && finding.matched_line) byMemory.set(key, finding);
|
|
1357
|
+
}
|
|
1358
|
+
return [...byMemory.values(), ...out];
|
|
1359
|
+
}
|
|
1360
|
+
function decideVerdict(input) {
|
|
1361
|
+
const { policy, stage, isAgent } = input;
|
|
1362
|
+
const process2 = processGateDecision(policy, stage, isAgent);
|
|
1363
|
+
const findings = input.findings.map((finding) => {
|
|
1364
|
+
if (finding.severity !== "error" || !PROCESS_GATE_CODES.has(finding.code)) return finding;
|
|
1365
|
+
if (process2.refuses) return finding;
|
|
1366
|
+
return {
|
|
1367
|
+
...finding,
|
|
1368
|
+
severity: "warn",
|
|
1369
|
+
// Capped so a downgraded gate cannot dominate the health score it no longer refuses on.
|
|
1370
|
+
impact: Math.min(finding.impact ?? 8, 8),
|
|
1371
|
+
reason: process2.reason,
|
|
1372
|
+
message: `${finding.message} (${process2.reason})`
|
|
1373
|
+
};
|
|
1374
|
+
});
|
|
1375
|
+
const baselineHealth = computeBaselineHealth(findings, policy.scoreThreshold);
|
|
1376
|
+
const refusals = dedupeRefusals(findings);
|
|
1377
|
+
const hasErrors = findings.some((f) => f.severity === "error");
|
|
1378
|
+
return {
|
|
1379
|
+
findings,
|
|
1380
|
+
should_block: policy.mode === "strict" && hasErrors,
|
|
1381
|
+
actor: isAgent ? `agent (${(input.agentSignals ?? []).join(", ")})` : process2.refuses ? "human \u2014 strict (enforcement.humanCommits)" : "human \u2014 process gates relaxed",
|
|
1382
|
+
baseline_health: baselineHealth,
|
|
1383
|
+
refusals,
|
|
1384
|
+
process_gate_reason: process2.reason
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
function buildBaselineHealthFinding(findings, health, shouldBlock) {
|
|
1388
|
+
if (shouldBlock || health.score >= health.threshold) return null;
|
|
1389
|
+
const topPenalties = findings.filter((f) => !CONTENT_CODES.has(f.code)).map((f) => ({ code: f.code, penalty: penaltyOf(f) })).filter((p) => p.penalty > 0).sort((a, b) => b.penalty - a.penalty).slice(0, 3);
|
|
1390
|
+
return {
|
|
1391
|
+
severity: "warn",
|
|
1392
|
+
code: "enforcement-score-below-threshold",
|
|
1393
|
+
message: `Repo knowledge-layer health ${health.score}% is below the ${health.threshold}% target` + (topPenalties.length > 0 ? ` \u2014 top gaps: ${topPenalties.map((p) => `${p.code} (\u2212${p.penalty})`).join(", ")}` : "") + ". This measures the repo's baseline, not this change; it never blocks.",
|
|
1394
|
+
fix: "Fill the gaps above (bootstrap, briefing, recap), then rerun `hivelore enforce check`.",
|
|
1395
|
+
impact: 0
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// src/anchor-specificity.ts
|
|
1400
|
+
var WEAK_ANCHOR_CHURN_RATIO = 0.35;
|
|
1401
|
+
var DEFAULT_SPECIFICITY = 1;
|
|
1402
|
+
function anchorSpecificity(matchedPaths, churn, totalCommits) {
|
|
1403
|
+
if (totalCommits <= 0 || matchedPaths.length === 0) return DEFAULT_SPECIFICITY;
|
|
1404
|
+
let best = 0;
|
|
1405
|
+
let sawKnown = false;
|
|
1406
|
+
for (const path22 of matchedPaths) {
|
|
1407
|
+
const touched = churn.get(normalizeChurnPath(path22));
|
|
1408
|
+
if (touched === void 0) continue;
|
|
1409
|
+
sawKnown = true;
|
|
1410
|
+
best = Math.max(best, 1 - Math.min(1, touched / totalCommits));
|
|
1411
|
+
}
|
|
1412
|
+
return sawKnown ? best : DEFAULT_SPECIFICITY;
|
|
1413
|
+
}
|
|
1414
|
+
function isWeakAnchor(specificity) {
|
|
1415
|
+
return specificity < 1 - WEAK_ANCHOR_CHURN_RATIO;
|
|
1416
|
+
}
|
|
1417
|
+
function normalizeChurnPath(value) {
|
|
1418
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1419
|
+
}
|
|
1420
|
+
function churnForAnchors(anchorPaths, fileChurn) {
|
|
1421
|
+
const out = /* @__PURE__ */ new Map();
|
|
1422
|
+
for (const raw of anchorPaths) {
|
|
1423
|
+
const anchor = normalizeChurnPath(raw);
|
|
1424
|
+
const direct = fileChurn.get(anchor);
|
|
1425
|
+
if (direct !== void 0) {
|
|
1426
|
+
out.set(anchor, direct);
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
let max = 0;
|
|
1430
|
+
let matched = false;
|
|
1431
|
+
for (const [file, count] of fileChurn) {
|
|
1432
|
+
if (!pathCoveredByAnchor(anchor, file)) continue;
|
|
1433
|
+
matched = true;
|
|
1434
|
+
max = Math.max(max, count);
|
|
1435
|
+
}
|
|
1436
|
+
if (matched) out.set(anchor, max);
|
|
1437
|
+
}
|
|
1438
|
+
return out;
|
|
1439
|
+
}
|
|
1440
|
+
function pathCoveredByAnchor(anchor, file) {
|
|
1441
|
+
if (anchor === file) return true;
|
|
1442
|
+
if (!anchor.includes("*")) return file.startsWith(`${anchor}/`);
|
|
1443
|
+
const pattern = anchor.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(//g, ".*");
|
|
1444
|
+
try {
|
|
1445
|
+
return new RegExp(`^${pattern}$`).test(file);
|
|
1446
|
+
} catch {
|
|
1447
|
+
return false;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function auditAnchorSpecificity(memories, fileChurn, totalCommits) {
|
|
1451
|
+
if (totalCommits <= 0) return [];
|
|
1452
|
+
const rows = [];
|
|
1453
|
+
for (const memory of memories) {
|
|
1454
|
+
if (memory.anchorPaths.length === 0) continue;
|
|
1455
|
+
const rolled = churnForAnchors(memory.anchorPaths, fileChurn);
|
|
1456
|
+
if (rolled.size === 0) continue;
|
|
1457
|
+
const specificity = anchorSpecificity(memory.anchorPaths, rolled, totalCommits);
|
|
1458
|
+
if (!isWeakAnchor(specificity)) continue;
|
|
1459
|
+
const broad = [...rolled].map(([path22, count]) => ({ path: path22, ratio: count / totalCommits })).filter((entry) => entry.ratio > WEAK_ANCHOR_CHURN_RATIO).sort((a, b) => b.ratio - a.ratio);
|
|
1460
|
+
rows.push({ id: memory.id, specificity, broad });
|
|
1461
|
+
}
|
|
1462
|
+
return rows.sort((a, b) => a.specificity - b.specificity);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1207
1465
|
// src/priority.ts
|
|
1208
1466
|
var DEFAULT_PRIORITY_SIGNALS = {
|
|
1209
1467
|
type: "",
|
|
@@ -1215,7 +1473,8 @@ var DEFAULT_PRIORITY_SIGNALS = {
|
|
|
1215
1473
|
strongSemantic: false,
|
|
1216
1474
|
usefulSemantic: false,
|
|
1217
1475
|
moduleOrDomainMatch: false,
|
|
1218
|
-
tagTaskMatch: false
|
|
1476
|
+
tagTaskMatch: false,
|
|
1477
|
+
anchorSpecificity: DEFAULT_SPECIFICITY
|
|
1219
1478
|
};
|
|
1220
1479
|
function prioritySignals(partial) {
|
|
1221
1480
|
return { ...DEFAULT_PRIORITY_SIGNALS, ...partial };
|
|
@@ -1223,9 +1482,13 @@ function prioritySignals(partial) {
|
|
|
1223
1482
|
function classifyMemoryPriority(signals) {
|
|
1224
1483
|
const isNegative = signals.type === "attempt";
|
|
1225
1484
|
const isSkill2 = signals.type === "skill";
|
|
1226
|
-
|
|
1485
|
+
const weakAnchor = signals.directAnchor && isWeakAnchor(signals.anchorSpecificity ?? DEFAULT_SPECIFICITY);
|
|
1486
|
+
const strongAnchor = signals.directAnchor && !weakAnchor;
|
|
1487
|
+
const corroborated = signals.strongSemantic || signals.directSymbol;
|
|
1488
|
+
if (signals.requiresHumanApproval || strongAnchor || signals.directSymbol || weakAnchor && corroborated || isNegative && (signals.exactTaskMatch || signals.strongSemantic) || isSkill2 && (signals.exactTaskMatch || signals.strongSemantic)) {
|
|
1227
1489
|
return "must_read";
|
|
1228
1490
|
}
|
|
1491
|
+
if (weakAnchor) return "useful";
|
|
1229
1492
|
if (isStackPackSeed({ tags: signals.tags }) || isEnvWorkaroundMemory({ tags: signals.tags })) {
|
|
1230
1493
|
if (isStackPackSeed({ tags: signals.tags }) && (signals.exactTaskMatch || signals.strongSemantic)) {
|
|
1231
1494
|
return "useful";
|
|
@@ -1394,10 +1657,10 @@ function sensorPatternBrittleness(pattern) {
|
|
|
1394
1657
|
function normalizeProjectPath(value) {
|
|
1395
1658
|
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^[ab]\//, "").replace(/\/+$/g, "");
|
|
1396
1659
|
}
|
|
1397
|
-
function sensorAppliesToPath(sensor, anchorPaths,
|
|
1660
|
+
function sensorAppliesToPath(sensor, anchorPaths, path22) {
|
|
1398
1661
|
const scopes = sensor.paths.length > 0 ? sensor.paths : anchorPaths;
|
|
1399
1662
|
if (scopes.length === 0) return true;
|
|
1400
|
-
const target = normalizeProjectPath(
|
|
1663
|
+
const target = normalizeProjectPath(path22);
|
|
1401
1664
|
return scopes.some((rawScope) => {
|
|
1402
1665
|
const scope = normalizeProjectPath(rawScope);
|
|
1403
1666
|
if (!scope) return false;
|
|
@@ -1722,6 +1985,34 @@ function judgeProposedSensor(sensor, input) {
|
|
|
1722
1985
|
}
|
|
1723
1986
|
return { accepted: true, self_check, brittle };
|
|
1724
1987
|
}
|
|
1988
|
+
function explainSensorRejection(verdict, context) {
|
|
1989
|
+
const retry = context.style === "cli" ? "re-run" : "re-propose";
|
|
1990
|
+
switch (verdict.reason) {
|
|
1991
|
+
case "fires-on-current": {
|
|
1992
|
+
const where = verdict.self_check.fired_on.join(", ");
|
|
1993
|
+
const warnCommand = context.style === "cli" ? `hivelore sensors propose ${context.memoryId ?? "<memory-id>"} --pattern '<same>' --severity warn` : `propose_sensor({ memory_id: "${context.memoryId ?? "<memory-id>"}", pattern: "<same>", severity: "warn" })`;
|
|
1994
|
+
return [
|
|
1995
|
+
`A block sensor must be silent on the current code, and this one fires on: ${where}.`,
|
|
1996
|
+
"That means one of two things:",
|
|
1997
|
+
` 1. The faulty pattern is STILL PRESENT \u2014 the usual case when you document a problem before`,
|
|
1998
|
+
` fixing it. A block sensor cannot be armed yet. Arm it as a warning now, fix the code,`,
|
|
1999
|
+
` then promote it:`,
|
|
2000
|
+
` ${warnCommand}`,
|
|
2001
|
+
` \u2026then re-run the same proposal with severity "block" once ${where} is clean.`,
|
|
2002
|
+
` 2. The pattern also matches LEGITIMATE usage. Add or tighten the 'absent' companion so`,
|
|
2003
|
+
` correct usage is excluded, then ${retry}.`
|
|
2004
|
+
].join("\n");
|
|
2005
|
+
}
|
|
2006
|
+
case "fires-on-correct":
|
|
2007
|
+
return `Inverted: the pattern matches the lesson's OWN recommended fix (its \`Instead, use:\` approach) \u2014 it would block correct code and never the mistake. Point the pattern at the FAULTY usage, then ${retry}.`;
|
|
2008
|
+
case "missed-bad-example":
|
|
2009
|
+
return `The sensor did not match the bad example, so it won't catch the mistake. Adjust the pattern, then ${retry}.`;
|
|
2010
|
+
case "brittle":
|
|
2011
|
+
return `The pattern is brittle (${verdict.brittle}). Use a durable pattern (avoid hardcoded line numbers), then ${retry}.`;
|
|
2012
|
+
default:
|
|
2013
|
+
return `Re-propose with a discriminating pattern, then ${retry}.`;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
1725
2016
|
function isHarnessErrorOutput(output) {
|
|
1726
2017
|
if (!output) return false;
|
|
1727
2018
|
return HARNESS_ERROR_SIGNATURES.some((re) => re.test(output));
|
|
@@ -1884,7 +2175,33 @@ function suggestSensorSeed(body, anchorPaths, options = {}) {
|
|
|
1884
2175
|
message: companion ? companionMessage(body, companion) : sensorMessageFromBody(body, token)
|
|
1885
2176
|
};
|
|
1886
2177
|
if (companion) seed.absent = escapeRegExp(companion.required);
|
|
1887
|
-
return seed;
|
|
2178
|
+
return seedFiresOnCorrectUsage(seed, body) ? null : seed;
|
|
2179
|
+
}
|
|
2180
|
+
function seedFiresOnCorrectUsage(seed, body) {
|
|
2181
|
+
const correct = correctUsageText(body);
|
|
2182
|
+
if (!correct) return false;
|
|
2183
|
+
let re;
|
|
2184
|
+
try {
|
|
2185
|
+
re = new RegExp(seed.pattern, "m");
|
|
2186
|
+
} catch {
|
|
2187
|
+
return true;
|
|
2188
|
+
}
|
|
2189
|
+
if (!re.test(correct)) return false;
|
|
2190
|
+
if (seed.absent) {
|
|
2191
|
+
try {
|
|
2192
|
+
if (new RegExp(seed.absent, "m").test(correct)) return false;
|
|
2193
|
+
} catch {
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
return true;
|
|
2197
|
+
}
|
|
2198
|
+
function correctUsageText(body) {
|
|
2199
|
+
const parts = [...extractCorrectApproachExamples(body)];
|
|
2200
|
+
const headingRe = /^#{1,6}\s+(?:how to apply|instead|correct|the fix|fix|recommended|do this|guidance)\b[^\n]*\n([\s\S]*?)(?=^#{1,6}\s|(?![\s\S]))/gim;
|
|
2201
|
+
for (const match of body.matchAll(headingRe)) {
|
|
2202
|
+
if (match[1]) parts.push(match[1]);
|
|
2203
|
+
}
|
|
2204
|
+
return parts.join("\n").trim();
|
|
1888
2205
|
}
|
|
1889
2206
|
function suggestSensorFromMemory(body, anchorPaths, options = {}) {
|
|
1890
2207
|
const seed = suggestSensorSeed(body, anchorPaths, options);
|
|
@@ -2649,10 +2966,11 @@ function allocateBudget(parts, maxTokens) {
|
|
|
2649
2966
|
}
|
|
2650
2967
|
|
|
2651
2968
|
// src/code-map.ts
|
|
2652
|
-
import { mkdir as
|
|
2653
|
-
import {
|
|
2969
|
+
import { mkdir as mkdir5, readFile as readFile7, readdir as readdir3, stat as stat2, writeFile as writeFile4 } from "fs/promises";
|
|
2970
|
+
import { createHash as createHash2 } from "crypto";
|
|
2971
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2654
2972
|
import { spawnSync } from "child_process";
|
|
2655
|
-
import
|
|
2973
|
+
import path9 from "path";
|
|
2656
2974
|
|
|
2657
2975
|
// src/ast-parser.ts
|
|
2658
2976
|
import { fileURLToPath } from "url";
|
|
@@ -3064,27 +3382,53 @@ var CODE_MAP_DEFAULT_EXCLUDE = [
|
|
|
3064
3382
|
];
|
|
3065
3383
|
var TEST_FILE_RE = /\.(test|spec)\.[a-z]+$/i;
|
|
3066
3384
|
function codeMapPath(paths) {
|
|
3067
|
-
return
|
|
3385
|
+
return path9.join(paths.haiveDir, CODE_MAP_FILE);
|
|
3386
|
+
}
|
|
3387
|
+
function codeMapMetaPath(paths) {
|
|
3388
|
+
return path9.join(paths.runtimeDir, "code-map-meta.json");
|
|
3389
|
+
}
|
|
3390
|
+
function serializeCodeMap(map) {
|
|
3391
|
+
const files = {};
|
|
3392
|
+
for (const key of Object.keys(map.files).sort()) files[key] = map.files[key];
|
|
3393
|
+
return `${JSON.stringify({ version: map.version, files }, null, 2)}
|
|
3394
|
+
`;
|
|
3395
|
+
}
|
|
3396
|
+
function codeMapContentHash(map) {
|
|
3397
|
+
return createHash2("sha256").update(serializeCodeMap(map)).digest("hex").slice(0, 16);
|
|
3068
3398
|
}
|
|
3069
3399
|
async function loadCodeMap(paths) {
|
|
3070
3400
|
const file = codeMapPath(paths);
|
|
3071
|
-
if (!
|
|
3072
|
-
|
|
3401
|
+
if (!existsSync7(file)) return null;
|
|
3402
|
+
const parsed = JSON.parse(await readFile7(file, "utf8"));
|
|
3403
|
+
const generatedAt = parsed.generated_at ?? await readFile7(codeMapMetaPath(paths), "utf8").then((raw) => JSON.parse(raw).generated_at).catch(() => void 0) ?? await stat2(file).then((s) => s.mtime.toISOString()).catch(() => (/* @__PURE__ */ new Date(0)).toISOString());
|
|
3404
|
+
return { ...parsed, root: parsed.root ?? paths.root, generated_at: generatedAt };
|
|
3073
3405
|
}
|
|
3074
3406
|
async function saveCodeMap(paths, map) {
|
|
3075
3407
|
const file = codeMapPath(paths);
|
|
3076
|
-
|
|
3077
|
-
await
|
|
3408
|
+
const payload = serializeCodeMap(map);
|
|
3409
|
+
const current = existsSync7(file) ? await readFile7(file, "utf8").catch(() => null) : null;
|
|
3410
|
+
if (current === payload) return;
|
|
3411
|
+
await mkdir5(path9.dirname(file), { recursive: true });
|
|
3412
|
+
await writeFile4(file, payload, "utf8");
|
|
3413
|
+
await mkdir5(paths.runtimeDir, { recursive: true }).catch(() => {
|
|
3414
|
+
});
|
|
3415
|
+
await writeFile4(
|
|
3416
|
+
codeMapMetaPath(paths),
|
|
3417
|
+
`${JSON.stringify({ generated_at: (/* @__PURE__ */ new Date()).toISOString(), content_hash: codeMapContentHash(map) }, null, 2)}
|
|
3418
|
+
`,
|
|
3419
|
+
"utf8"
|
|
3420
|
+
).catch(() => {
|
|
3421
|
+
});
|
|
3078
3422
|
}
|
|
3079
3423
|
async function buildCodeMap(root, options = {}) {
|
|
3080
3424
|
const include = new Set(options.includeExtensions ?? CODE_MAP_DEFAULT_INCLUDE);
|
|
3081
3425
|
const exclude = new Set(options.excludeDirs ?? CODE_MAP_DEFAULT_EXCLUDE);
|
|
3082
3426
|
const files = {};
|
|
3083
3427
|
for await (const abs of collectSourceFiles(root, include, exclude, options.includeUntracked)) {
|
|
3084
|
-
const rel =
|
|
3428
|
+
const rel = path9.relative(root, abs).replace(/\\/g, "/");
|
|
3085
3429
|
if (rel.startsWith(".ai/")) continue;
|
|
3086
|
-
const content = await
|
|
3087
|
-
const ext =
|
|
3430
|
+
const content = await readFile7(abs, "utf8");
|
|
3431
|
+
const ext = path9.extname(abs).toLowerCase();
|
|
3088
3432
|
const entry = await parseFileEntry(content, ext);
|
|
3089
3433
|
if (entry.exports.length > 0) files[rel] = entry;
|
|
3090
3434
|
}
|
|
@@ -3105,11 +3449,11 @@ async function countSourceFilesOnDisk(root, options = {}) {
|
|
|
3105
3449
|
async function* collectSourceFiles(root, include, exclude, includeUntracked) {
|
|
3106
3450
|
const gitFiles = gitSourceFiles(root, include, exclude, includeUntracked === true);
|
|
3107
3451
|
if (gitFiles) {
|
|
3108
|
-
for (const rel of gitFiles) yield
|
|
3452
|
+
for (const rel of gitFiles) yield path9.join(root, rel);
|
|
3109
3453
|
for await (const nested of findNestedGitRepos(root, exclude)) {
|
|
3110
3454
|
const nestedFiles = gitSourceFiles(nested, include, exclude, includeUntracked === true);
|
|
3111
3455
|
if (nestedFiles) {
|
|
3112
|
-
for (const rel of nestedFiles) yield
|
|
3456
|
+
for (const rel of nestedFiles) yield path9.join(nested, rel);
|
|
3113
3457
|
}
|
|
3114
3458
|
}
|
|
3115
3459
|
return;
|
|
@@ -3128,8 +3472,8 @@ async function* findNestedGitRepos(root, exclude, depth = 0) {
|
|
|
3128
3472
|
if (!entry.isDirectory()) continue;
|
|
3129
3473
|
if (entry.name.startsWith(".")) continue;
|
|
3130
3474
|
if (exclude.has(entry.name)) continue;
|
|
3131
|
-
const full =
|
|
3132
|
-
if (
|
|
3475
|
+
const full = path9.join(root, entry.name);
|
|
3476
|
+
if (existsSync7(path9.join(full, ".git"))) {
|
|
3133
3477
|
yield full;
|
|
3134
3478
|
} else {
|
|
3135
3479
|
yield* findNestedGitRepos(full, exclude, depth + 1);
|
|
@@ -3158,11 +3502,11 @@ async function* walkSourceFiles(dir, include, exclude) {
|
|
|
3158
3502
|
if (entry.isDirectory()) continue;
|
|
3159
3503
|
}
|
|
3160
3504
|
if (exclude.has(entry.name)) continue;
|
|
3161
|
-
const full =
|
|
3505
|
+
const full = path9.join(dir, entry.name);
|
|
3162
3506
|
if (entry.isDirectory()) {
|
|
3163
3507
|
yield* walkSourceFiles(full, include, exclude);
|
|
3164
3508
|
} else if (entry.isFile()) {
|
|
3165
|
-
const ext =
|
|
3509
|
+
const ext = path9.extname(entry.name).toLowerCase();
|
|
3166
3510
|
if (include.has(ext) && !TEST_FILE_RE.test(entry.name)) yield full;
|
|
3167
3511
|
}
|
|
3168
3512
|
}
|
|
@@ -3173,7 +3517,7 @@ function isIncludedSourcePath(rel, include, exclude) {
|
|
|
3173
3517
|
const parts = normalized.split("/");
|
|
3174
3518
|
if (parts.some((part) => exclude.has(part))) return false;
|
|
3175
3519
|
const base = parts.at(-1) ?? "";
|
|
3176
|
-
const ext =
|
|
3520
|
+
const ext = path9.extname(base).toLowerCase();
|
|
3177
3521
|
return include.has(ext) && !TEST_FILE_RE.test(base);
|
|
3178
3522
|
}
|
|
3179
3523
|
var EXPORT_RE = /(?:^|;)[ \t]*export\s+(?:default\s+)?(async\s+)?(function|class|interface|type|const|let|var|enum)\s+(\*?)\s*([A-Za-z_$][\w$]*)/gm;
|
|
@@ -3435,10 +3779,10 @@ function queryCodeMap(map, options) {
|
|
|
3435
3779
|
}
|
|
3436
3780
|
|
|
3437
3781
|
// src/config.ts
|
|
3438
|
-
import { existsSync as
|
|
3782
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3439
3783
|
import { readFileSync } from "fs";
|
|
3440
|
-
import { readFile as
|
|
3441
|
-
import
|
|
3784
|
+
import { readFile as readFile8, rm, writeFile as writeFile5 } from "fs/promises";
|
|
3785
|
+
import path10 from "path";
|
|
3442
3786
|
var CONFIG_FILE = "hivelore.config.json";
|
|
3443
3787
|
var LEGACY_CONFIG_FILE = "haive.config.json";
|
|
3444
3788
|
var DEFAULT_BRIEFING_EXCLUDE_TAGS = [
|
|
@@ -3534,19 +3878,19 @@ function antiPatternGateParams(gate) {
|
|
|
3534
3878
|
}
|
|
3535
3879
|
}
|
|
3536
3880
|
function configPath(paths) {
|
|
3537
|
-
return
|
|
3881
|
+
return path10.join(paths.haiveDir, CONFIG_FILE);
|
|
3538
3882
|
}
|
|
3539
3883
|
function resolveConfigPath(paths) {
|
|
3540
3884
|
const current = configPath(paths);
|
|
3541
|
-
if (
|
|
3542
|
-
const legacy =
|
|
3543
|
-
return
|
|
3885
|
+
if (existsSync8(current)) return current;
|
|
3886
|
+
const legacy = path10.join(paths.haiveDir, LEGACY_CONFIG_FILE);
|
|
3887
|
+
return existsSync8(legacy) ? legacy : current;
|
|
3544
3888
|
}
|
|
3545
3889
|
async function loadConfig(paths) {
|
|
3546
3890
|
const file = resolveConfigPath(paths);
|
|
3547
|
-
if (!
|
|
3891
|
+
if (!existsSync8(file)) return { ...DEFAULT_CONFIG };
|
|
3548
3892
|
try {
|
|
3549
|
-
const raw = await
|
|
3893
|
+
const raw = await readFile8(file, "utf8");
|
|
3550
3894
|
const parsed = JSON.parse(raw);
|
|
3551
3895
|
const merged = mergeConfig(DEFAULT_CONFIG, parsed);
|
|
3552
3896
|
if (merged.autopilot) {
|
|
@@ -3559,7 +3903,7 @@ async function loadConfig(paths) {
|
|
|
3559
3903
|
}
|
|
3560
3904
|
function loadConfigSync(paths) {
|
|
3561
3905
|
const file = resolveConfigPath(paths);
|
|
3562
|
-
if (!
|
|
3906
|
+
if (!existsSync8(file)) return { ...DEFAULT_CONFIG };
|
|
3563
3907
|
try {
|
|
3564
3908
|
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
3565
3909
|
const merged = mergeConfig(DEFAULT_CONFIG, parsed);
|
|
@@ -3569,9 +3913,9 @@ function loadConfigSync(paths) {
|
|
|
3569
3913
|
}
|
|
3570
3914
|
}
|
|
3571
3915
|
async function saveConfig(paths, config) {
|
|
3572
|
-
await
|
|
3573
|
-
const legacy =
|
|
3574
|
-
if (
|
|
3916
|
+
await writeFile5(configPath(paths), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3917
|
+
const legacy = path10.join(paths.haiveDir, LEGACY_CONFIG_FILE);
|
|
3918
|
+
if (existsSync8(legacy)) {
|
|
3575
3919
|
try {
|
|
3576
3920
|
await rm(legacy, { force: true });
|
|
3577
3921
|
} catch {
|
|
@@ -3594,21 +3938,21 @@ function mergeConfig(base, override) {
|
|
|
3594
3938
|
}
|
|
3595
3939
|
|
|
3596
3940
|
// src/cross-repo.ts
|
|
3597
|
-
import { existsSync as
|
|
3598
|
-
import { mkdir as
|
|
3599
|
-
import
|
|
3941
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3942
|
+
import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
|
|
3943
|
+
import path11 from "path";
|
|
3600
3944
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3601
3945
|
async function loadImportMap(cacheDir) {
|
|
3602
|
-
const mapPath =
|
|
3603
|
-
if (!
|
|
3946
|
+
const mapPath = path11.join(cacheDir, "import-map.json");
|
|
3947
|
+
if (!existsSync9(mapPath)) return {};
|
|
3604
3948
|
try {
|
|
3605
|
-
return JSON.parse(await
|
|
3949
|
+
return JSON.parse(await readFile9(mapPath, "utf8"));
|
|
3606
3950
|
} catch {
|
|
3607
3951
|
return {};
|
|
3608
3952
|
}
|
|
3609
3953
|
}
|
|
3610
3954
|
async function saveImportMap(cacheDir, map) {
|
|
3611
|
-
await
|
|
3955
|
+
await writeFile6(path11.join(cacheDir, "import-map.json"), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
3612
3956
|
}
|
|
3613
3957
|
async function pullCrossRepoSources(paths, config, projectRoot) {
|
|
3614
3958
|
const sources = config.crossRepoSources ?? [];
|
|
@@ -3629,8 +3973,8 @@ async function pullFromSource(paths, source, projectRoot) {
|
|
|
3629
3973
|
};
|
|
3630
3974
|
let sourceRoot = null;
|
|
3631
3975
|
if (source.path) {
|
|
3632
|
-
const resolved =
|
|
3633
|
-
if (!
|
|
3976
|
+
const resolved = path11.resolve(projectRoot, source.path);
|
|
3977
|
+
if (!existsSync9(resolved)) {
|
|
3634
3978
|
report.errors.push(`Path not found: ${resolved}`);
|
|
3635
3979
|
return report;
|
|
3636
3980
|
}
|
|
@@ -3643,7 +3987,7 @@ async function pullFromSource(paths, source, projectRoot) {
|
|
|
3643
3987
|
return report;
|
|
3644
3988
|
}
|
|
3645
3989
|
const sourcePaths = resolveHaivePaths(sourceRoot);
|
|
3646
|
-
if (!
|
|
3990
|
+
if (!existsSync9(sourcePaths.memoriesDir)) {
|
|
3647
3991
|
report.errors.push(`No .ai/memories/ found at ${sourceRoot}`);
|
|
3648
3992
|
return report;
|
|
3649
3993
|
}
|
|
@@ -3666,10 +4010,10 @@ async function pullFromSource(paths, source, projectRoot) {
|
|
|
3666
4010
|
report.skipped.push("no shared memories found in source");
|
|
3667
4011
|
return report;
|
|
3668
4012
|
}
|
|
3669
|
-
const destDir =
|
|
3670
|
-
await
|
|
3671
|
-
const cacheDir =
|
|
3672
|
-
await
|
|
4013
|
+
const destDir = path11.join(paths.memoriesDir, "shared", source.name);
|
|
4014
|
+
await mkdir6(destDir, { recursive: true });
|
|
4015
|
+
const cacheDir = path11.join(paths.haiveDir, ".cache", "cross-repo", source.name);
|
|
4016
|
+
await mkdir6(cacheDir, { recursive: true });
|
|
3673
4017
|
const importMap = await loadImportMap(cacheDir);
|
|
3674
4018
|
const mapDirty = false;
|
|
3675
4019
|
let dirty = mapDirty;
|
|
@@ -3683,7 +4027,7 @@ async function pullFromSource(paths, source, projectRoot) {
|
|
|
3683
4027
|
|
|
3684
4028
|
`;
|
|
3685
4029
|
const existingLocalPath = importMap[sourceId];
|
|
3686
|
-
if (existingLocalPath &&
|
|
4030
|
+
if (existingLocalPath && existsSync9(existingLocalPath)) {
|
|
3687
4031
|
const existingFiles = await loadMemoriesFromDir(destDir);
|
|
3688
4032
|
const existingEntry = existingFiles.find(({ filePath }) => filePath === existingLocalPath);
|
|
3689
4033
|
const sourceBodyStripped = memory.body.trim();
|
|
@@ -3694,7 +4038,7 @@ async function pullFromSource(paths, source, projectRoot) {
|
|
|
3694
4038
|
}
|
|
3695
4039
|
const updatedBody = importedBodyPrefix + memory.body;
|
|
3696
4040
|
if (existingEntry) {
|
|
3697
|
-
await
|
|
4041
|
+
await writeFile6(
|
|
3698
4042
|
existingLocalPath,
|
|
3699
4043
|
serializeMemory({ frontmatter: existingEntry.memory.frontmatter, body: updatedBody }),
|
|
3700
4044
|
"utf8"
|
|
@@ -3718,8 +4062,8 @@ async function pullFromSource(paths, source, projectRoot) {
|
|
|
3718
4062
|
topic: fm.topic ? `${source.name}:${fm.topic}` : void 0
|
|
3719
4063
|
});
|
|
3720
4064
|
const body = importedBodyPrefix + memory.body;
|
|
3721
|
-
const destPath =
|
|
3722
|
-
await
|
|
4065
|
+
const destPath = path11.join(destDir, `${newFm.id}.md`);
|
|
4066
|
+
await writeFile6(destPath, serializeMemory({ frontmatter: newFm, body }), "utf8");
|
|
3723
4067
|
importMap[sourceId] = destPath;
|
|
3724
4068
|
dirty = true;
|
|
3725
4069
|
report.imported.push(sourceId);
|
|
@@ -3729,9 +4073,9 @@ async function pullFromSource(paths, source, projectRoot) {
|
|
|
3729
4073
|
return report;
|
|
3730
4074
|
}
|
|
3731
4075
|
async function cloneOrFetchGitSource(source, paths, report) {
|
|
3732
|
-
const cacheDir =
|
|
3733
|
-
await
|
|
3734
|
-
if (
|
|
4076
|
+
const cacheDir = path11.join(paths.haiveDir, ".cache", "cross-repo", source.name);
|
|
4077
|
+
await mkdir6(cacheDir, { recursive: true });
|
|
4078
|
+
if (existsSync9(path11.join(cacheDir, ".git"))) {
|
|
3735
4079
|
const result = spawnSync2("git", ["fetch", "--depth=1", "origin"], {
|
|
3736
4080
|
cwd: cacheDir,
|
|
3737
4081
|
encoding: "utf8"
|
|
@@ -3756,9 +4100,9 @@ async function cloneOrFetchGitSource(source, paths, report) {
|
|
|
3756
4100
|
}
|
|
3757
4101
|
|
|
3758
4102
|
// src/dep-tracker.ts
|
|
3759
|
-
import { existsSync as
|
|
3760
|
-
import { readFile as
|
|
3761
|
-
import
|
|
4103
|
+
import { existsSync as existsSync10 } from "fs";
|
|
4104
|
+
import { readFile as readFile10, writeFile as writeFile7, mkdir as mkdir7 } from "fs/promises";
|
|
4105
|
+
import path12 from "path";
|
|
3762
4106
|
function parsePackageJson(content) {
|
|
3763
4107
|
try {
|
|
3764
4108
|
const pkg = JSON.parse(content);
|
|
@@ -3830,7 +4174,7 @@ var KNOWN_MANIFESTS = [
|
|
|
3830
4174
|
{ name: "pom.xml", parser: parsePomXml }
|
|
3831
4175
|
];
|
|
3832
4176
|
function getParser(file) {
|
|
3833
|
-
const base =
|
|
4177
|
+
const base = path12.basename(file);
|
|
3834
4178
|
return KNOWN_MANIFESTS.find((m) => m.name === base)?.parser ?? null;
|
|
3835
4179
|
}
|
|
3836
4180
|
function extractMajor(version) {
|
|
@@ -3848,32 +4192,32 @@ function isMajorBump(from, to) {
|
|
|
3848
4192
|
}
|
|
3849
4193
|
function resolveManifestFiles(projectRoot, configuredFiles) {
|
|
3850
4194
|
if (configuredFiles !== void 0) {
|
|
3851
|
-
return configuredFiles.map((f) =>
|
|
4195
|
+
return configuredFiles.map((f) => path12.resolve(projectRoot, f)).filter(existsSync10);
|
|
3852
4196
|
}
|
|
3853
|
-
return KNOWN_MANIFESTS.map(({ name }) =>
|
|
4197
|
+
return KNOWN_MANIFESTS.map(({ name }) => path12.join(projectRoot, name)).filter(existsSync10);
|
|
3854
4198
|
}
|
|
3855
4199
|
async function trackDependencies(projectRoot, haiveDir, manifestFiles) {
|
|
3856
|
-
const contractsDir =
|
|
3857
|
-
await
|
|
4200
|
+
const contractsDir = path12.join(haiveDir, "contracts");
|
|
4201
|
+
await mkdir7(contractsDir, { recursive: true });
|
|
3858
4202
|
const results = [];
|
|
3859
4203
|
for (const manifestPath of manifestFiles) {
|
|
3860
4204
|
const parser2 = getParser(manifestPath);
|
|
3861
4205
|
if (!parser2) continue;
|
|
3862
|
-
const content = await
|
|
4206
|
+
const content = await readFile10(manifestPath, "utf8");
|
|
3863
4207
|
const currentDeps = parser2(content);
|
|
3864
|
-
const lockName = `deps-${
|
|
3865
|
-
const lockPath =
|
|
3866
|
-
if (!
|
|
4208
|
+
const lockName = `deps-${path12.basename(manifestPath)}.lock`;
|
|
4209
|
+
const lockPath = path12.join(contractsDir, lockName);
|
|
4210
|
+
if (!existsSync10(lockPath)) {
|
|
3867
4211
|
const snapshot2 = {
|
|
3868
|
-
file:
|
|
3869
|
-
format:
|
|
4212
|
+
file: path12.relative(projectRoot, manifestPath),
|
|
4213
|
+
format: path12.basename(manifestPath),
|
|
3870
4214
|
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3871
4215
|
deps: currentDeps
|
|
3872
4216
|
};
|
|
3873
|
-
await
|
|
4217
|
+
await writeFile7(lockPath, JSON.stringify(snapshot2, null, 2) + "\n", "utf8");
|
|
3874
4218
|
continue;
|
|
3875
4219
|
}
|
|
3876
|
-
const snapshot = JSON.parse(await
|
|
4220
|
+
const snapshot = JSON.parse(await readFile10(lockPath, "utf8"));
|
|
3877
4221
|
const changes = [];
|
|
3878
4222
|
for (const [name, currentVer] of Object.entries(currentDeps)) {
|
|
3879
4223
|
const prevVer = snapshot.deps[name];
|
|
@@ -3887,22 +4231,22 @@ async function trackDependencies(projectRoot, haiveDir, manifestFiles) {
|
|
|
3887
4231
|
}
|
|
3888
4232
|
}
|
|
3889
4233
|
if (changes.length > 0) {
|
|
3890
|
-
results.push({ file:
|
|
4234
|
+
results.push({ file: path12.relative(projectRoot, manifestPath), changes });
|
|
3891
4235
|
const updated = {
|
|
3892
4236
|
...snapshot,
|
|
3893
4237
|
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3894
4238
|
deps: currentDeps
|
|
3895
4239
|
};
|
|
3896
|
-
await
|
|
4240
|
+
await writeFile7(lockPath, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
3897
4241
|
}
|
|
3898
4242
|
}
|
|
3899
4243
|
return results;
|
|
3900
4244
|
}
|
|
3901
4245
|
|
|
3902
4246
|
// src/contract-watcher.ts
|
|
3903
|
-
import { existsSync as
|
|
3904
|
-
import { readFile as
|
|
3905
|
-
import
|
|
4247
|
+
import { existsSync as existsSync11 } from "fs";
|
|
4248
|
+
import { readFile as readFile11, writeFile as writeFile8, mkdir as mkdir8 } from "fs/promises";
|
|
4249
|
+
import path13 from "path";
|
|
3906
4250
|
import crypto from "crypto";
|
|
3907
4251
|
function sha256(content) {
|
|
3908
4252
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
@@ -4105,14 +4449,14 @@ function diffSnapshots(before, after) {
|
|
|
4105
4449
|
return changes;
|
|
4106
4450
|
}
|
|
4107
4451
|
function contractLockPath(haiveDir, name) {
|
|
4108
|
-
return
|
|
4452
|
+
return path13.join(haiveDir, "contracts", `${name}.lock`);
|
|
4109
4453
|
}
|
|
4110
4454
|
async function snapshotContract(projectRoot, haiveDir, contract) {
|
|
4111
|
-
const filePath =
|
|
4112
|
-
if (!
|
|
4455
|
+
const filePath = path13.resolve(projectRoot, contract.path);
|
|
4456
|
+
if (!existsSync11(filePath)) {
|
|
4113
4457
|
throw new Error(`Contract file not found: ${filePath}`);
|
|
4114
4458
|
}
|
|
4115
|
-
const content = await
|
|
4459
|
+
const content = await readFile11(filePath, "utf8");
|
|
4116
4460
|
const parsed = parseByFormat(content, contract.format, filePath);
|
|
4117
4461
|
const snapshot = {
|
|
4118
4462
|
name: contract.name,
|
|
@@ -4122,23 +4466,23 @@ async function snapshotContract(projectRoot, haiveDir, contract) {
|
|
|
4122
4466
|
hash: sha256(content),
|
|
4123
4467
|
...parsed
|
|
4124
4468
|
};
|
|
4125
|
-
const contractsDir =
|
|
4126
|
-
await
|
|
4127
|
-
await
|
|
4469
|
+
const contractsDir = path13.join(haiveDir, "contracts");
|
|
4470
|
+
await mkdir8(contractsDir, { recursive: true });
|
|
4471
|
+
await writeFile8(contractLockPath(haiveDir, contract.name), JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
4128
4472
|
return snapshot;
|
|
4129
4473
|
}
|
|
4130
4474
|
async function diffContract(projectRoot, haiveDir, contract) {
|
|
4131
|
-
const filePath =
|
|
4132
|
-
if (!
|
|
4475
|
+
const filePath = path13.resolve(projectRoot, contract.path);
|
|
4476
|
+
if (!existsSync11(filePath)) {
|
|
4133
4477
|
return { contract: contract.name, file: contract.path, changes: [], unchanged: true };
|
|
4134
4478
|
}
|
|
4135
4479
|
const lockPath = contractLockPath(haiveDir, contract.name);
|
|
4136
|
-
if (!
|
|
4480
|
+
if (!existsSync11(lockPath)) {
|
|
4137
4481
|
await snapshotContract(projectRoot, haiveDir, contract);
|
|
4138
4482
|
return { contract: contract.name, file: contract.path, changes: [], unchanged: true };
|
|
4139
4483
|
}
|
|
4140
|
-
const content = await
|
|
4141
|
-
const beforeSnapshot = JSON.parse(await
|
|
4484
|
+
const content = await readFile11(filePath, "utf8");
|
|
4485
|
+
const beforeSnapshot = JSON.parse(await readFile11(lockPath, "utf8"));
|
|
4142
4486
|
const afterParsed = parseByFormat(content, contract.format, filePath);
|
|
4143
4487
|
const afterSnapshot = {
|
|
4144
4488
|
...beforeSnapshot,
|
|
@@ -4148,7 +4492,7 @@ async function diffContract(projectRoot, haiveDir, contract) {
|
|
|
4148
4492
|
};
|
|
4149
4493
|
const changes = diffSnapshots(beforeSnapshot, afterSnapshot);
|
|
4150
4494
|
if (changes.length > 0) {
|
|
4151
|
-
await
|
|
4495
|
+
await writeFile8(lockPath, JSON.stringify(afterSnapshot, null, 2) + "\n", "utf8");
|
|
4152
4496
|
}
|
|
4153
4497
|
return {
|
|
4154
4498
|
contract: contract.name,
|
|
@@ -4166,27 +4510,27 @@ async function watchContracts(projectRoot, haiveDir, contractFiles) {
|
|
|
4166
4510
|
}
|
|
4167
4511
|
|
|
4168
4512
|
// src/usage-log.ts
|
|
4169
|
-
import { appendFile as appendFile2, mkdir as
|
|
4170
|
-
import { existsSync as
|
|
4171
|
-
import
|
|
4513
|
+
import { appendFile as appendFile2, mkdir as mkdir9, readFile as readFile12, stat as stat3 } from "fs/promises";
|
|
4514
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4515
|
+
import path14 from "path";
|
|
4172
4516
|
var USAGE_LOG_FILE = "tool-usage.jsonl";
|
|
4173
4517
|
var USAGE_LOG_DIR = ".usage";
|
|
4174
4518
|
function usageLogPath(paths) {
|
|
4175
|
-
return
|
|
4519
|
+
return path14.join(paths.haiveDir, USAGE_LOG_DIR, USAGE_LOG_FILE);
|
|
4176
4520
|
}
|
|
4177
4521
|
async function appendUsageEvent(paths, event) {
|
|
4178
4522
|
try {
|
|
4179
4523
|
const file = usageLogPath(paths);
|
|
4180
|
-
const dir =
|
|
4181
|
-
if (!
|
|
4524
|
+
const dir = path14.dirname(file);
|
|
4525
|
+
if (!existsSync12(dir)) await mkdir9(dir, { recursive: true });
|
|
4182
4526
|
await appendFile2(file, JSON.stringify(event) + "\n", "utf8");
|
|
4183
4527
|
} catch {
|
|
4184
4528
|
}
|
|
4185
4529
|
}
|
|
4186
4530
|
async function readUsageEvents(paths) {
|
|
4187
4531
|
const file = usageLogPath(paths);
|
|
4188
|
-
if (!
|
|
4189
|
-
const raw = await
|
|
4532
|
+
if (!existsSync12(file)) return [];
|
|
4533
|
+
const raw = await readFile12(file, "utf8");
|
|
4190
4534
|
const out = [];
|
|
4191
4535
|
for (const line of raw.split("\n")) {
|
|
4192
4536
|
if (!line) continue;
|
|
@@ -4235,25 +4579,25 @@ function parseSince(input) {
|
|
|
4235
4579
|
}
|
|
4236
4580
|
async function usageLogSize(paths) {
|
|
4237
4581
|
const file = usageLogPath(paths);
|
|
4238
|
-
if (!
|
|
4239
|
-
const st = await
|
|
4240
|
-
const raw = await
|
|
4582
|
+
if (!existsSync12(file)) return { exists: false, size_bytes: 0, lines: 0 };
|
|
4583
|
+
const st = await stat3(file);
|
|
4584
|
+
const raw = await readFile12(file, "utf8");
|
|
4241
4585
|
return { exists: true, size_bytes: st.size, lines: raw.split("\n").filter((l) => l).length };
|
|
4242
4586
|
}
|
|
4243
4587
|
|
|
4244
4588
|
// src/friction.ts
|
|
4245
|
-
import { appendFile as appendFile3, mkdir as
|
|
4246
|
-
import { existsSync as
|
|
4247
|
-
import { createHash as
|
|
4248
|
-
import
|
|
4589
|
+
import { appendFile as appendFile3, mkdir as mkdir10, readFile as readFile13, writeFile as writeFile9 } from "fs/promises";
|
|
4590
|
+
import { existsSync as existsSync13 } from "fs";
|
|
4591
|
+
import { createHash as createHash3 } from "crypto";
|
|
4592
|
+
import path15 from "path";
|
|
4249
4593
|
var FRICTION_LOG_FILE = "friction.jsonl";
|
|
4250
4594
|
var FRICTION_STATE_FILE = "friction-state.json";
|
|
4251
4595
|
var FRICTION_FIELD_MAX = 2e3;
|
|
4252
4596
|
function frictionLogPath(paths) {
|
|
4253
|
-
return
|
|
4597
|
+
return path15.join(paths.runtimeDir, FRICTION_LOG_FILE);
|
|
4254
4598
|
}
|
|
4255
4599
|
function frictionStatePath(paths) {
|
|
4256
|
-
return
|
|
4600
|
+
return path15.join(paths.runtimeDir, FRICTION_STATE_FILE);
|
|
4257
4601
|
}
|
|
4258
4602
|
function normalizeFrictionSummary(value) {
|
|
4259
4603
|
return value.toLowerCase().replace(/[a-z]?:?[\\/](?:[\w.-]+[\\/])+/g, "/").replace(/\d{3,}/g, "N").replace(/\s+/g, " ").trim();
|
|
@@ -4264,7 +4608,7 @@ function frictionFingerprint(input) {
|
|
|
4264
4608
|
input.surface.trim().toLowerCase(),
|
|
4265
4609
|
normalizeFrictionSummary(input.summary)
|
|
4266
4610
|
].join("|");
|
|
4267
|
-
return
|
|
4611
|
+
return createHash3("sha256").update(basis).digest("hex").slice(0, 16);
|
|
4268
4612
|
}
|
|
4269
4613
|
function truncate(value) {
|
|
4270
4614
|
if (value === void 0) return void 0;
|
|
@@ -4298,7 +4642,7 @@ async function appendFrictionReport(paths, input) {
|
|
|
4298
4642
|
};
|
|
4299
4643
|
const prior = (await readFrictionReports(paths)).filter((r) => r.fingerprint === fingerprint);
|
|
4300
4644
|
try {
|
|
4301
|
-
if (!
|
|
4645
|
+
if (!existsSync13(paths.runtimeDir)) await mkdir10(paths.runtimeDir, { recursive: true });
|
|
4302
4646
|
await appendFile3(frictionLogPath(paths), JSON.stringify(report) + "\n", "utf8");
|
|
4303
4647
|
} catch {
|
|
4304
4648
|
}
|
|
@@ -4312,10 +4656,10 @@ async function appendFrictionReport(paths, input) {
|
|
|
4312
4656
|
}
|
|
4313
4657
|
async function readFrictionReports(paths) {
|
|
4314
4658
|
const file = frictionLogPath(paths);
|
|
4315
|
-
if (!
|
|
4659
|
+
if (!existsSync13(file)) return [];
|
|
4316
4660
|
let raw;
|
|
4317
4661
|
try {
|
|
4318
|
-
raw = await
|
|
4662
|
+
raw = await readFile13(file, "utf8");
|
|
4319
4663
|
} catch {
|
|
4320
4664
|
return [];
|
|
4321
4665
|
}
|
|
@@ -4332,17 +4676,17 @@ async function readFrictionReports(paths) {
|
|
|
4332
4676
|
}
|
|
4333
4677
|
async function loadFrictionState(paths) {
|
|
4334
4678
|
const file = frictionStatePath(paths);
|
|
4335
|
-
if (!
|
|
4679
|
+
if (!existsSync13(file)) return {};
|
|
4336
4680
|
try {
|
|
4337
|
-
const parsed = JSON.parse(await
|
|
4681
|
+
const parsed = JSON.parse(await readFile13(file, "utf8"));
|
|
4338
4682
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
4339
4683
|
} catch {
|
|
4340
4684
|
return {};
|
|
4341
4685
|
}
|
|
4342
4686
|
}
|
|
4343
4687
|
async function saveFrictionState(paths, state) {
|
|
4344
|
-
if (!
|
|
4345
|
-
await
|
|
4688
|
+
if (!existsSync13(paths.runtimeDir)) await mkdir10(paths.runtimeDir, { recursive: true });
|
|
4689
|
+
await writeFile9(frictionStatePath(paths), JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
4346
4690
|
}
|
|
4347
4691
|
async function setFrictionStatus(paths, fingerprint, status, url) {
|
|
4348
4692
|
const state = await loadFrictionState(paths);
|
|
@@ -4479,21 +4823,21 @@ function extractActionsBriefBody(markdown, maxChars = MAX_DEFAULT_CHARS) {
|
|
|
4479
4823
|
}
|
|
4480
4824
|
|
|
4481
4825
|
// src/resolve-project.ts
|
|
4482
|
-
import { existsSync as
|
|
4483
|
-
import
|
|
4826
|
+
import { existsSync as existsSync14 } from "fs";
|
|
4827
|
+
import path16 from "path";
|
|
4484
4828
|
var ROOT_MARKERS2 = [".ai", ".git", "package.json"];
|
|
4485
4829
|
function markersAtRoot(root) {
|
|
4486
4830
|
const found = [];
|
|
4487
4831
|
for (const m of ROOT_MARKERS2) {
|
|
4488
|
-
if (
|
|
4832
|
+
if (existsSync14(path16.join(root, m))) found.push(m);
|
|
4489
4833
|
}
|
|
4490
4834
|
return found;
|
|
4491
4835
|
}
|
|
4492
4836
|
function resolveProjectInfo(opts = {}) {
|
|
4493
4837
|
const env = opts.env ?? process.env;
|
|
4494
|
-
const cwd =
|
|
4838
|
+
const cwd = path16.resolve(opts.cwd ?? process.cwd());
|
|
4495
4839
|
const raw = env.HAIVE_PROJECT_ROOT;
|
|
4496
|
-
const explicit = raw !== void 0 && raw !== "" ?
|
|
4840
|
+
const explicit = raw !== void 0 && raw !== "" ? path16.resolve(raw) : null;
|
|
4497
4841
|
const resolvedRoot = explicit ?? findProjectRoot(cwd);
|
|
4498
4842
|
const paths = resolveHaivePaths(resolvedRoot);
|
|
4499
4843
|
return {
|
|
@@ -4501,8 +4845,8 @@ function resolveProjectInfo(opts = {}) {
|
|
|
4501
4845
|
resolved_root: resolvedRoot,
|
|
4502
4846
|
haive_project_root_env: explicit,
|
|
4503
4847
|
explicit_root: explicit != null,
|
|
4504
|
-
haive_dir_exists:
|
|
4505
|
-
memories_dir_exists:
|
|
4848
|
+
haive_dir_exists: existsSync14(paths.haiveDir),
|
|
4849
|
+
memories_dir_exists: existsSync14(paths.memoriesDir),
|
|
4506
4850
|
runtime_dir: paths.runtimeDir,
|
|
4507
4851
|
markers_found: markersAtRoot(resolvedRoot)
|
|
4508
4852
|
};
|
|
@@ -4757,16 +5101,16 @@ function findLexicalConflictPairs(memories, opts) {
|
|
|
4757
5101
|
}
|
|
4758
5102
|
|
|
4759
5103
|
// src/runtime-journal.ts
|
|
4760
|
-
import { mkdir as
|
|
4761
|
-
import { existsSync as
|
|
4762
|
-
import
|
|
5104
|
+
import { mkdir as mkdir11, readFile as readFile14, appendFile as appendFile4 } from "fs/promises";
|
|
5105
|
+
import { existsSync as existsSync15 } from "fs";
|
|
5106
|
+
import path17 from "path";
|
|
4763
5107
|
var RUNTIME_JOURNAL_FILENAME = "session-journal.ndjson";
|
|
4764
5108
|
function runtimeJournalPath(paths) {
|
|
4765
|
-
return
|
|
5109
|
+
return path17.join(paths.runtimeDir, RUNTIME_JOURNAL_FILENAME);
|
|
4766
5110
|
}
|
|
4767
5111
|
async function appendRuntimeJournalEntry(paths, entry) {
|
|
4768
5112
|
try {
|
|
4769
|
-
await
|
|
5113
|
+
await mkdir11(paths.runtimeDir, { recursive: true });
|
|
4770
5114
|
const line = {
|
|
4771
5115
|
ts: entry.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
4772
5116
|
kind: entry.kind,
|
|
@@ -4784,9 +5128,9 @@ async function appendRuntimeJournalEntry(paths, entry) {
|
|
|
4784
5128
|
}
|
|
4785
5129
|
async function readRuntimeJournalTail(paths, limit) {
|
|
4786
5130
|
const file = runtimeJournalPath(paths);
|
|
4787
|
-
if (!
|
|
5131
|
+
if (!existsSync15(file) || limit <= 0) return [];
|
|
4788
5132
|
try {
|
|
4789
|
-
const raw = await
|
|
5133
|
+
const raw = await readFile14(file, "utf8");
|
|
4790
5134
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4791
5135
|
const parsed = [];
|
|
4792
5136
|
for (const line of lines.slice(-limit)) {
|
|
@@ -4802,22 +5146,22 @@ async function readRuntimeJournalTail(paths, limit) {
|
|
|
4802
5146
|
}
|
|
4803
5147
|
|
|
4804
5148
|
// src/enforcement.ts
|
|
4805
|
-
import { mkdir as
|
|
4806
|
-
import { existsSync as
|
|
4807
|
-
import
|
|
5149
|
+
import { mkdir as mkdir12, readdir as readdir4, readFile as readFile15, writeFile as writeFile10 } from "fs/promises";
|
|
5150
|
+
import { existsSync as existsSync16 } from "fs";
|
|
5151
|
+
import path18 from "path";
|
|
4808
5152
|
var BRIEFING_MARKER_TTL_MS = 12 * 60 * 60 * 1e3;
|
|
4809
5153
|
var SESSION_RECAP_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4810
5154
|
function enforcementDir(paths) {
|
|
4811
|
-
return
|
|
5155
|
+
return path18.join(paths.runtimeDir, "enforcement");
|
|
4812
5156
|
}
|
|
4813
5157
|
function briefingMarkersDir(paths) {
|
|
4814
|
-
return
|
|
5158
|
+
return path18.join(enforcementDir(paths), "briefings");
|
|
4815
5159
|
}
|
|
4816
5160
|
function normalizeSessionId(sessionId) {
|
|
4817
5161
|
return (sessionId?.trim() || "default").replace(/[^a-zA-Z0-9_.-]+/g, "-").slice(0, 120);
|
|
4818
5162
|
}
|
|
4819
5163
|
function briefingMarkerPath(paths, sessionId) {
|
|
4820
|
-
return
|
|
5164
|
+
return path18.join(briefingMarkersDir(paths), `${normalizeSessionId(sessionId)}.json`);
|
|
4821
5165
|
}
|
|
4822
5166
|
async function writeBriefingMarker(paths, input) {
|
|
4823
5167
|
const sessionId = normalizeSessionId(input.sessionId);
|
|
@@ -4842,8 +5186,8 @@ async function writeBriefingMarker(paths, input) {
|
|
|
4842
5186
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4843
5187
|
root: paths.root
|
|
4844
5188
|
};
|
|
4845
|
-
await
|
|
4846
|
-
await
|
|
5189
|
+
await mkdir12(briefingMarkersDir(paths), { recursive: true });
|
|
5190
|
+
await writeFile10(
|
|
4847
5191
|
briefingMarkerPath(paths, marker.session_id),
|
|
4848
5192
|
JSON.stringify(marker, null, 2) + "\n",
|
|
4849
5193
|
"utf8"
|
|
@@ -4852,9 +5196,9 @@ async function writeBriefingMarker(paths, input) {
|
|
|
4852
5196
|
}
|
|
4853
5197
|
async function readSessionBriefingMarker(paths, sessionId, ttlMs = BRIEFING_MARKER_TTL_MS) {
|
|
4854
5198
|
const file = briefingMarkerPath(paths, sessionId);
|
|
4855
|
-
if (!
|
|
5199
|
+
if (!existsSync16(file)) return null;
|
|
4856
5200
|
try {
|
|
4857
|
-
const marker = JSON.parse(await
|
|
5201
|
+
const marker = JSON.parse(await readFile15(file, "utf8"));
|
|
4858
5202
|
const created = Date.parse(marker.created_at);
|
|
4859
5203
|
if (!Number.isFinite(created) || Date.now() - created > ttlMs) return null;
|
|
4860
5204
|
return marker;
|
|
@@ -4866,18 +5210,18 @@ async function hasRecentBriefingMarker(paths, sessionId, ttlMs = BRIEFING_MARKER
|
|
|
4866
5210
|
const now = Date.now();
|
|
4867
5211
|
const candidates = [];
|
|
4868
5212
|
const exact = briefingMarkerPath(paths, sessionId);
|
|
4869
|
-
if (
|
|
5213
|
+
if (existsSync16(exact)) candidates.push(exact);
|
|
4870
5214
|
try {
|
|
4871
5215
|
const dir = briefingMarkersDir(paths);
|
|
4872
5216
|
const files = await readdir4(dir);
|
|
4873
5217
|
for (const file of files) {
|
|
4874
|
-
if (file.endsWith(".json")) candidates.push(
|
|
5218
|
+
if (file.endsWith(".json")) candidates.push(path18.join(dir, file));
|
|
4875
5219
|
}
|
|
4876
5220
|
} catch {
|
|
4877
5221
|
}
|
|
4878
5222
|
for (const file of new Set(candidates)) {
|
|
4879
5223
|
try {
|
|
4880
|
-
const marker = JSON.parse(await
|
|
5224
|
+
const marker = JSON.parse(await readFile15(file, "utf8"));
|
|
4881
5225
|
const created = Date.parse(marker.created_at);
|
|
4882
5226
|
if (Number.isFinite(created) && now - created <= ttlMs) return true;
|
|
4883
5227
|
} catch {
|
|
@@ -4889,12 +5233,12 @@ async function readRecentBriefingMarker(paths, sessionId, ttlMs = BRIEFING_MARKE
|
|
|
4889
5233
|
const now = Date.now();
|
|
4890
5234
|
const candidates = [];
|
|
4891
5235
|
const exact = briefingMarkerPath(paths, sessionId);
|
|
4892
|
-
if (
|
|
5236
|
+
if (existsSync16(exact)) candidates.push(exact);
|
|
4893
5237
|
try {
|
|
4894
5238
|
const dir = briefingMarkersDir(paths);
|
|
4895
5239
|
const files = await readdir4(dir);
|
|
4896
5240
|
for (const file of files) {
|
|
4897
|
-
if (file.endsWith(".json")) candidates.push(
|
|
5241
|
+
if (file.endsWith(".json")) candidates.push(path18.join(dir, file));
|
|
4898
5242
|
}
|
|
4899
5243
|
} catch {
|
|
4900
5244
|
}
|
|
@@ -4902,7 +5246,7 @@ async function readRecentBriefingMarker(paths, sessionId, ttlMs = BRIEFING_MARKE
|
|
|
4902
5246
|
let freshestTs = 0;
|
|
4903
5247
|
for (const file of new Set(candidates)) {
|
|
4904
5248
|
try {
|
|
4905
|
-
const marker = JSON.parse(await
|
|
5249
|
+
const marker = JSON.parse(await readFile15(file, "utf8"));
|
|
4906
5250
|
const created = Date.parse(marker.created_at);
|
|
4907
5251
|
if (!Number.isFinite(created) || now - created > ttlMs) continue;
|
|
4908
5252
|
if (created > freshestTs) {
|
|
@@ -4952,15 +5296,15 @@ function isRetiredMemory(fm, body = "", now = /* @__PURE__ */ new Date()) {
|
|
|
4952
5296
|
}
|
|
4953
5297
|
|
|
4954
5298
|
// src/sensor-ledger.ts
|
|
4955
|
-
import { createHash as
|
|
4956
|
-
import { existsSync as
|
|
4957
|
-
import { appendFile as appendFile5, mkdir as
|
|
4958
|
-
import
|
|
5299
|
+
import { createHash as createHash4 } from "crypto";
|
|
5300
|
+
import { existsSync as existsSync17, readFileSync as readFileSync2 } from "fs";
|
|
5301
|
+
import { appendFile as appendFile5, mkdir as mkdir13, readFile as readFile16, rename, writeFile as writeFile11 } from "fs/promises";
|
|
5302
|
+
import path19 from "path";
|
|
4959
5303
|
var MAX_LINES = 1e4;
|
|
4960
5304
|
var RETAINED_LINES = 8e3;
|
|
4961
5305
|
var DAY_MS = 864e5;
|
|
4962
5306
|
function sensorLedgerPath(paths) {
|
|
4963
|
-
return
|
|
5307
|
+
return path19.join(paths.runtimeDir, "enforcement", "sensor-ledger.ndjson");
|
|
4964
5308
|
}
|
|
4965
5309
|
function isEvaluation(value) {
|
|
4966
5310
|
if (!value || typeof value !== "object") return false;
|
|
@@ -4971,13 +5315,13 @@ async function appendSensorEvaluations(paths, evaluations) {
|
|
|
4971
5315
|
if (evaluations.length === 0) return;
|
|
4972
5316
|
try {
|
|
4973
5317
|
const file = sensorLedgerPath(paths);
|
|
4974
|
-
await
|
|
5318
|
+
await mkdir13(path19.dirname(file), { recursive: true });
|
|
4975
5319
|
await appendFile5(file, evaluations.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf8");
|
|
4976
|
-
const raw = await
|
|
5320
|
+
const raw = await readFile16(file, "utf8");
|
|
4977
5321
|
const lines = raw.split("\n").filter(Boolean);
|
|
4978
5322
|
if (lines.length > MAX_LINES) {
|
|
4979
5323
|
const temp = `${file}.${process.pid}.tmp`;
|
|
4980
|
-
await
|
|
5324
|
+
await writeFile11(temp, lines.slice(-RETAINED_LINES).join("\n") + "\n", "utf8");
|
|
4981
5325
|
await rename(temp, file);
|
|
4982
5326
|
}
|
|
4983
5327
|
} catch {
|
|
@@ -4986,9 +5330,9 @@ async function appendSensorEvaluations(paths, evaluations) {
|
|
|
4986
5330
|
async function loadSensorLedger(paths, opts = {}) {
|
|
4987
5331
|
try {
|
|
4988
5332
|
const file = sensorLedgerPath(paths);
|
|
4989
|
-
if (!
|
|
5333
|
+
if (!existsSync17(file)) return [];
|
|
4990
5334
|
const since = opts.since ? Date.parse(opts.since) : Number.NEGATIVE_INFINITY;
|
|
4991
|
-
const raw = await
|
|
5335
|
+
const raw = await readFile16(file, "utf8");
|
|
4992
5336
|
const out = [];
|
|
4993
5337
|
for (const line of raw.split("\n")) {
|
|
4994
5338
|
if (!line.trim()) continue;
|
|
@@ -5010,11 +5354,11 @@ function computeScopeHash(root, scopedFiles) {
|
|
|
5010
5354
|
try {
|
|
5011
5355
|
const files = [...new Set(scopedFiles.map((f) => f.replace(/\\/g, "/")))].sort();
|
|
5012
5356
|
if (files.length === 0) return "";
|
|
5013
|
-
const hash =
|
|
5357
|
+
const hash = createHash4("sha256");
|
|
5014
5358
|
let included = 0;
|
|
5015
5359
|
for (const rel of files) {
|
|
5016
|
-
const abs =
|
|
5017
|
-
if (!
|
|
5360
|
+
const abs = path19.resolve(root, rel);
|
|
5361
|
+
if (!existsSync17(abs)) continue;
|
|
5018
5362
|
try {
|
|
5019
5363
|
hash.update(rel);
|
|
5020
5364
|
hash.update("\0");
|
|
@@ -5748,8 +6092,8 @@ function normalizeFindingSeverity(raw) {
|
|
|
5748
6092
|
return "info";
|
|
5749
6093
|
}
|
|
5750
6094
|
}
|
|
5751
|
-
function findingKey(tool, ruleId,
|
|
5752
|
-
return `${tool}:${ruleId}:${
|
|
6095
|
+
function findingKey(tool, ruleId, path22) {
|
|
6096
|
+
return `${tool}:${ruleId}:${path22}`;
|
|
5753
6097
|
}
|
|
5754
6098
|
function coerceJson(input) {
|
|
5755
6099
|
if (typeof input === "string") {
|
|
@@ -5783,8 +6127,8 @@ function parseSarif(input) {
|
|
|
5783
6127
|
const physical = asRecord(location.physicalLocation);
|
|
5784
6128
|
const artifact = asRecord(physical.artifactLocation);
|
|
5785
6129
|
const region = asRecord(physical.region);
|
|
5786
|
-
const
|
|
5787
|
-
if (!
|
|
6130
|
+
const path22 = typeof artifact.uri === "string" ? normalizeUri(artifact.uri) : "";
|
|
6131
|
+
if (!path22) continue;
|
|
5788
6132
|
const line = typeof region.startLine === "number" ? region.startLine : void 0;
|
|
5789
6133
|
const snippet = typeof asRecord(region.snippet).text === "string" ? asRecord(region.snippet).text.trim() : void 0;
|
|
5790
6134
|
findings.push({
|
|
@@ -5792,10 +6136,10 @@ function parseSarif(input) {
|
|
|
5792
6136
|
ruleId,
|
|
5793
6137
|
message: message.trim(),
|
|
5794
6138
|
severity,
|
|
5795
|
-
path:
|
|
6139
|
+
path: path22,
|
|
5796
6140
|
...line !== void 0 ? { line } : {},
|
|
5797
6141
|
...snippet ? { snippet } : {},
|
|
5798
|
-
key: findingKey(tool, ruleId,
|
|
6142
|
+
key: findingKey(tool, ruleId, path22)
|
|
5799
6143
|
});
|
|
5800
6144
|
}
|
|
5801
6145
|
}
|
|
@@ -5814,17 +6158,17 @@ function parseSonar(input) {
|
|
|
5814
6158
|
(typeof issue.severity === "string" ? issue.severity : void 0) ?? impactSeverity
|
|
5815
6159
|
);
|
|
5816
6160
|
const component = typeof issue.component === "string" ? issue.component : "";
|
|
5817
|
-
const
|
|
5818
|
-
if (!
|
|
6161
|
+
const path22 = componentToPath(component);
|
|
6162
|
+
if (!path22) continue;
|
|
5819
6163
|
const line = typeof issue.line === "number" ? issue.line : void 0;
|
|
5820
6164
|
findings.push({
|
|
5821
6165
|
tool: "sonar",
|
|
5822
6166
|
ruleId,
|
|
5823
6167
|
message,
|
|
5824
6168
|
severity,
|
|
5825
|
-
path:
|
|
6169
|
+
path: path22,
|
|
5826
6170
|
...line !== void 0 ? { line } : {},
|
|
5827
|
-
key: findingKey("sonar", ruleId,
|
|
6171
|
+
key: findingKey("sonar", ruleId, path22)
|
|
5828
6172
|
});
|
|
5829
6173
|
}
|
|
5830
6174
|
return findings;
|
|
@@ -5837,7 +6181,7 @@ function parseEslintJson(input, opts = {}) {
|
|
|
5837
6181
|
const file = asRecord(fileRaw);
|
|
5838
6182
|
const rawPath = typeof file.filePath === "string" ? file.filePath : "";
|
|
5839
6183
|
if (!rawPath) continue;
|
|
5840
|
-
const
|
|
6184
|
+
const path22 = cwd && rawPath.startsWith(cwd) ? rawPath.slice(cwd.length) : rawPath;
|
|
5841
6185
|
for (const msgRaw of asArray(file.messages)) {
|
|
5842
6186
|
const msg = asRecord(msgRaw);
|
|
5843
6187
|
const ruleId = typeof msg.ruleId === "string" && msg.ruleId ? msg.ruleId : "parse-error";
|
|
@@ -5849,9 +6193,9 @@ function parseEslintJson(input, opts = {}) {
|
|
|
5849
6193
|
ruleId,
|
|
5850
6194
|
message,
|
|
5851
6195
|
severity,
|
|
5852
|
-
path:
|
|
6196
|
+
path: path22,
|
|
5853
6197
|
...line !== void 0 ? { line } : {},
|
|
5854
|
-
key: findingKey("eslint", ruleId,
|
|
6198
|
+
key: findingKey("eslint", ruleId, path22)
|
|
5855
6199
|
});
|
|
5856
6200
|
}
|
|
5857
6201
|
}
|
|
@@ -6301,7 +6645,7 @@ function tallyHotFiles(paths, source = "agent") {
|
|
|
6301
6645
|
if (!norm) continue;
|
|
6302
6646
|
counts.set(norm, (counts.get(norm) ?? 0) + 1);
|
|
6303
6647
|
}
|
|
6304
|
-
return [...counts.entries()].map(([
|
|
6648
|
+
return [...counts.entries()].map(([path22, changes]) => ({ path: path22, changes, source })).sort((a, b) => b.changes - a.changes);
|
|
6305
6649
|
}
|
|
6306
6650
|
function mergeHotFiles(a, b) {
|
|
6307
6651
|
const merged = /* @__PURE__ */ new Map();
|
|
@@ -6321,21 +6665,21 @@ function mergeHotFiles(a, b) {
|
|
|
6321
6665
|
}
|
|
6322
6666
|
|
|
6323
6667
|
// src/eval-history.ts
|
|
6324
|
-
import { appendFile as appendFile6, mkdir as
|
|
6325
|
-
import { existsSync as
|
|
6326
|
-
import
|
|
6668
|
+
import { appendFile as appendFile6, mkdir as mkdir14, readFile as readFile17 } from "fs/promises";
|
|
6669
|
+
import { existsSync as existsSync18 } from "fs";
|
|
6670
|
+
import path20 from "path";
|
|
6327
6671
|
function evalHistoryPath(paths) {
|
|
6328
|
-
return
|
|
6672
|
+
return path20.join(paths.haiveDir, ".cache", "eval-history.jsonl");
|
|
6329
6673
|
}
|
|
6330
6674
|
async function appendEvalHistory(paths, entry) {
|
|
6331
6675
|
const file = evalHistoryPath(paths);
|
|
6332
|
-
await
|
|
6676
|
+
await mkdir14(path20.dirname(file), { recursive: true });
|
|
6333
6677
|
await appendFile6(file, JSON.stringify(entry) + "\n", "utf8");
|
|
6334
6678
|
}
|
|
6335
6679
|
async function loadEvalHistory(paths) {
|
|
6336
6680
|
const file = evalHistoryPath(paths);
|
|
6337
|
-
if (!
|
|
6338
|
-
const raw = await
|
|
6681
|
+
if (!existsSync18(file)) return [];
|
|
6682
|
+
const raw = await readFile17(file, "utf8").catch(() => "");
|
|
6339
6683
|
const out = [];
|
|
6340
6684
|
for (const line of raw.split("\n")) {
|
|
6341
6685
|
const trimmed = line.trim();
|
|
@@ -6678,12 +7022,12 @@ ${trimmed}`;
|
|
|
6678
7022
|
}
|
|
6679
7023
|
|
|
6680
7024
|
// src/handoff.ts
|
|
6681
|
-
import { writeFile as
|
|
6682
|
-
import { existsSync as
|
|
6683
|
-
import
|
|
7025
|
+
import { writeFile as writeFile12, readFile as readFile18, stat as stat4 } from "fs/promises";
|
|
7026
|
+
import { existsSync as existsSync19 } from "fs";
|
|
7027
|
+
import path21 from "path";
|
|
6684
7028
|
var HANDOFF_FILENAME = "NEXT.md";
|
|
6685
7029
|
function handoffFilePath(root) {
|
|
6686
|
-
return
|
|
7030
|
+
return path21.join(root, HANDOFF_FILENAME);
|
|
6687
7031
|
}
|
|
6688
7032
|
function buildHandoffMarkdown(data) {
|
|
6689
7033
|
const at = (data.at ?? /* @__PURE__ */ new Date()).toISOString();
|
|
@@ -6732,20 +7076,20 @@ function buildHandoffMarkdown(data) {
|
|
|
6732
7076
|
}
|
|
6733
7077
|
async function writeSessionHandoff(root, data) {
|
|
6734
7078
|
const file = handoffFilePath(root);
|
|
6735
|
-
await
|
|
7079
|
+
await writeFile12(file, buildHandoffMarkdown(data), "utf8");
|
|
6736
7080
|
return file;
|
|
6737
7081
|
}
|
|
6738
7082
|
async function readSessionHandoff(root) {
|
|
6739
7083
|
const file = handoffFilePath(root);
|
|
6740
|
-
if (!
|
|
6741
|
-
const raw = await
|
|
7084
|
+
if (!existsSync19(file)) return null;
|
|
7085
|
+
const raw = await readFile18(file, "utf8").catch(() => "");
|
|
6742
7086
|
return raw.trim() ? raw : null;
|
|
6743
7087
|
}
|
|
6744
7088
|
async function handoffAgeMs(root, now = /* @__PURE__ */ new Date()) {
|
|
6745
7089
|
const file = handoffFilePath(root);
|
|
6746
|
-
if (!
|
|
7090
|
+
if (!existsSync19(file)) return null;
|
|
6747
7091
|
try {
|
|
6748
|
-
const s = await
|
|
7092
|
+
const s = await stat4(file);
|
|
6749
7093
|
return Math.max(0, now.getTime() - s.mtimeMs);
|
|
6750
7094
|
} catch {
|
|
6751
7095
|
return null;
|
|
@@ -6870,6 +7214,7 @@ export {
|
|
|
6870
7214
|
CODE_MAP_FILE,
|
|
6871
7215
|
CODE_STOPWORDS,
|
|
6872
7216
|
CONFIG_FILE,
|
|
7217
|
+
CONTENT_CATCH_CODES,
|
|
6873
7218
|
CrossRepoProvenanceSchema,
|
|
6874
7219
|
DECAY_DAYS,
|
|
6875
7220
|
DEFAULT_AUTO_PROMOTE_RULE,
|
|
@@ -6877,11 +7222,14 @@ export {
|
|
|
6877
7222
|
DEFAULT_CONFIDENCE_THRESHOLDS,
|
|
6878
7223
|
DEFAULT_CONFIG,
|
|
6879
7224
|
DEFAULT_DORMANT_DAYS,
|
|
7225
|
+
DEFAULT_POSTURE,
|
|
6880
7226
|
DEFAULT_PRIORITY_SIGNALS,
|
|
7227
|
+
DEFAULT_SPECIFICITY,
|
|
6881
7228
|
ENV_WORKAROUND_TAGS,
|
|
6882
7229
|
FRICTION_FIELD_MAX,
|
|
6883
7230
|
FRICTION_LOG_FILE,
|
|
6884
7231
|
FRICTION_STATE_FILE,
|
|
7232
|
+
GATE_REMINDER_WINDOW_MS,
|
|
6885
7233
|
GUESSABLE_THRESHOLD,
|
|
6886
7234
|
HAIVE_DIR,
|
|
6887
7235
|
HAIVE_OWNED_FILES,
|
|
@@ -6895,6 +7243,8 @@ export {
|
|
|
6895
7243
|
MemoryStatusSchema,
|
|
6896
7244
|
MemoryTypeSchema,
|
|
6897
7245
|
PREVENTION_DEBOUNCE_MS,
|
|
7246
|
+
PREVENTION_RECEIPT_MARKER,
|
|
7247
|
+
PROCESS_GATE_CODES,
|
|
6898
7248
|
PROJECT_CONTEXT_FILE,
|
|
6899
7249
|
PROJECT_CONTEXT_THROTTLE_MS,
|
|
6900
7250
|
REVIEW_LEARNING_MARKER,
|
|
@@ -6904,12 +7254,14 @@ export {
|
|
|
6904
7254
|
SENSOR_ABSENT_LOOKBACK,
|
|
6905
7255
|
SENSOR_ABSENT_WINDOW,
|
|
6906
7256
|
SESSION_RECAP_TTL_MS,
|
|
7257
|
+
SETUP_GATE_CODES,
|
|
6907
7258
|
STACK_PACK_TAG,
|
|
6908
7259
|
SensorSchema,
|
|
6909
7260
|
TEST_FRAMEWORKS,
|
|
6910
7261
|
USAGE_FILE,
|
|
6911
7262
|
USAGE_LOG_DIR,
|
|
6912
7263
|
USAGE_LOG_FILE,
|
|
7264
|
+
WEAK_ANCHOR_CHURN_RATIO,
|
|
6913
7265
|
addedLineNumbersFromDiff,
|
|
6914
7266
|
addedLinesFromDiff,
|
|
6915
7267
|
aggregateRetrieval,
|
|
@@ -6917,6 +7269,7 @@ export {
|
|
|
6917
7269
|
aggregateUsage,
|
|
6918
7270
|
allocateBudget,
|
|
6919
7271
|
anchorMatchesComponent,
|
|
7272
|
+
anchorSpecificity,
|
|
6920
7273
|
antiPatternGateParams,
|
|
6921
7274
|
appendEvalHistory,
|
|
6922
7275
|
appendFrictionReport,
|
|
@@ -6932,10 +7285,12 @@ export {
|
|
|
6932
7285
|
assessBootstrapState,
|
|
6933
7286
|
assessScaffoldLoop,
|
|
6934
7287
|
assessSensorHealth,
|
|
7288
|
+
auditAnchorSpecificity,
|
|
6935
7289
|
bridgeMemorySummary,
|
|
6936
7290
|
briefingMarkerPath,
|
|
6937
7291
|
briefingMarkersDir,
|
|
6938
7292
|
briefingProofLine,
|
|
7293
|
+
buildBaselineHealthFinding,
|
|
6939
7294
|
buildCodeMap,
|
|
6940
7295
|
buildCoverageIndex,
|
|
6941
7296
|
buildDashboard,
|
|
@@ -6946,7 +7301,9 @@ export {
|
|
|
6946
7301
|
buildProposeCommand,
|
|
6947
7302
|
buildReport,
|
|
6948
7303
|
bumpRead,
|
|
7304
|
+
churnForAnchors,
|
|
6949
7305
|
classifyMemoryPriority,
|
|
7306
|
+
codeMapContentHash,
|
|
6950
7307
|
codeMapPath,
|
|
6951
7308
|
collectTimelineEntries,
|
|
6952
7309
|
compactAutoRecapBody,
|
|
@@ -6955,6 +7312,7 @@ export {
|
|
|
6955
7312
|
compareImpact,
|
|
6956
7313
|
compileRegexSensor,
|
|
6957
7314
|
componentOf,
|
|
7315
|
+
computeBaselineHealth,
|
|
6958
7316
|
computeEvalTrend,
|
|
6959
7317
|
computeGatePrecision,
|
|
6960
7318
|
computeImpact,
|
|
@@ -6964,8 +7322,11 @@ export {
|
|
|
6964
7322
|
configPath,
|
|
6965
7323
|
contractLockPath,
|
|
6966
7324
|
countSourceFilesOnDisk,
|
|
7325
|
+
decideVerdict,
|
|
7326
|
+
dedupeRefusals,
|
|
6967
7327
|
deriveConfidence,
|
|
6968
7328
|
deriveMainAreas,
|
|
7329
|
+
describePosture,
|
|
6969
7330
|
detectAgentContext,
|
|
6970
7331
|
detectSensorWeakening,
|
|
6971
7332
|
detectStacksFromManifests,
|
|
@@ -6981,6 +7342,7 @@ export {
|
|
|
6981
7342
|
evalHistoryPath,
|
|
6982
7343
|
evaluateSkillActivation,
|
|
6983
7344
|
existingGateMissShas,
|
|
7345
|
+
explainSensorRejection,
|
|
6984
7346
|
extractActionsBriefBody,
|
|
6985
7347
|
extractCorrectApproachExamples,
|
|
6986
7348
|
extractReferencedPaths,
|
|
@@ -7033,6 +7395,7 @@ export {
|
|
|
7033
7395
|
isStackPackSeed,
|
|
7034
7396
|
isStylisticRule,
|
|
7035
7397
|
isTemplateProjectContext,
|
|
7398
|
+
isWeakAnchor,
|
|
7036
7399
|
judgeProposedSensor,
|
|
7037
7400
|
lessonShortName,
|
|
7038
7401
|
listMarkdownFilesRecursive,
|
|
@@ -7059,6 +7422,7 @@ export {
|
|
|
7059
7422
|
mineSensorSeedFromDiff,
|
|
7060
7423
|
moduleNameOf,
|
|
7061
7424
|
newMemoryId,
|
|
7425
|
+
normalizeChurnPath,
|
|
7062
7426
|
normalizeFindingSeverity,
|
|
7063
7427
|
normalizeFramework,
|
|
7064
7428
|
normalizeFrictionSummary,
|
|
@@ -7098,6 +7462,7 @@ export {
|
|
|
7098
7462
|
readUsageEvents,
|
|
7099
7463
|
recommendFeedbackAdjustment,
|
|
7100
7464
|
recordApplied,
|
|
7465
|
+
recordGateReminder,
|
|
7101
7466
|
recordPrevention,
|
|
7102
7467
|
recordPreventionHits,
|
|
7103
7468
|
recordProjectContextEmission,
|
|
@@ -7106,10 +7471,12 @@ export {
|
|
|
7106
7471
|
renderBehaviourCoverageLine,
|
|
7107
7472
|
renderBootstrapChecklist,
|
|
7108
7473
|
renderCaughtForYou,
|
|
7474
|
+
renderPreventionComment,
|
|
7109
7475
|
renderPreventionReceipt,
|
|
7110
7476
|
renderPreventionReceiptShare,
|
|
7111
7477
|
resolveBriefingBudget,
|
|
7112
7478
|
resolveConfigPath,
|
|
7479
|
+
resolveGatePolicy,
|
|
7113
7480
|
resolveHaivePaths,
|
|
7114
7481
|
resolveManifestFiles,
|
|
7115
7482
|
resolveProjectInfo,
|
|
@@ -7137,8 +7504,10 @@ export {
|
|
|
7137
7504
|
sensorPromotedAtMap,
|
|
7138
7505
|
sensorSelfCheck,
|
|
7139
7506
|
sensorTargetsFromDiff,
|
|
7507
|
+
serializeCodeMap,
|
|
7140
7508
|
serializeMemory,
|
|
7141
7509
|
setFrictionStatus,
|
|
7510
|
+
shouldExpandGateReminder,
|
|
7142
7511
|
snapshotContract,
|
|
7143
7512
|
specificityScore,
|
|
7144
7513
|
stripPrivate,
|