@hivelore/core 0.34.1 → 0.35.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 +160 -2
- package/dist/index.js +338 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -54,7 +54,14 @@ var SensorSchema = z.object({
|
|
|
54
54
|
/** True when Hivelore generated this sensor automatically (vs. hand-authored). */
|
|
55
55
|
autogen: z.boolean().default(false),
|
|
56
56
|
/** ISO timestamp of the last time this sensor matched a diff. */
|
|
57
|
-
last_fired: z.string().nullable().default(null)
|
|
57
|
+
last_fired: z.string().nullable().default(null),
|
|
58
|
+
/**
|
|
59
|
+
* ISO timestamp of the last manual `sensors promote` back to block. Health assessment ignores
|
|
60
|
+
* ledger evaluations older than this — without it, a promoted sensor whose oracle was FIXED is
|
|
61
|
+
* re-quarantined on the next commit for up to 30 days (the stale flaps are still in the window),
|
|
62
|
+
* making the promotion promised by the quarantine note a no-op.
|
|
63
|
+
*/
|
|
64
|
+
promoted_at: z.string().optional()
|
|
58
65
|
});
|
|
59
66
|
var ActivationSchema = z.object({
|
|
60
67
|
/** Case-insensitive substrings matched against the task text. */
|
|
@@ -891,7 +898,7 @@ async function appendPreventionEvent(paths, event) {
|
|
|
891
898
|
await mkdir2(path6.dirname(file), { recursive: true });
|
|
892
899
|
await appendFile(file, JSON.stringify(event) + "\n", "utf8");
|
|
893
900
|
}
|
|
894
|
-
async function recordPreventionHits(paths, firedIds, source, now = /* @__PURE__ */ new Date()) {
|
|
901
|
+
async function recordPreventionHits(paths, firedIds, source, now = /* @__PURE__ */ new Date(), details = {}) {
|
|
895
902
|
const unique = [...new Set(firedIds)].filter(Boolean);
|
|
896
903
|
if (unique.length === 0) return [];
|
|
897
904
|
const usage = await loadUsageIndex(paths).catch(() => null);
|
|
@@ -905,11 +912,68 @@ async function recordPreventionHits(paths, firedIds, source, now = /* @__PURE__
|
|
|
905
912
|
});
|
|
906
913
|
const at = now.toISOString();
|
|
907
914
|
for (const id of recordedIds) {
|
|
908
|
-
await appendPreventionEvent(paths, { at, id, source }).catch(() => {
|
|
915
|
+
await appendPreventionEvent(paths, { at, id, source, ...details[id] }).catch(() => {
|
|
909
916
|
});
|
|
910
917
|
}
|
|
911
918
|
return recordedIds;
|
|
912
919
|
}
|
|
920
|
+
function buildPreventionReceipt(events, memories, usage, options) {
|
|
921
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
922
|
+
const sinceMs = options.since.getTime();
|
|
923
|
+
const nowMs = now.getTime();
|
|
924
|
+
const windowMs = Math.max(1, nowMs - sinceMs);
|
|
925
|
+
const previousSince = sinceMs - windowMs;
|
|
926
|
+
const byId = new Map(memories.map((m) => [m.memory.frontmatter.id, m]));
|
|
927
|
+
const current = events.filter((e) => {
|
|
928
|
+
const at = Date.parse(e.at);
|
|
929
|
+
return Number.isFinite(at) && at >= sinceMs && at <= nowMs;
|
|
930
|
+
});
|
|
931
|
+
const previousTotal = events.filter((e) => {
|
|
932
|
+
const at = Date.parse(e.at);
|
|
933
|
+
return Number.isFinite(at) && at >= previousSince && at < sinceMs;
|
|
934
|
+
}).length;
|
|
935
|
+
const rows = current.map((event) => {
|
|
936
|
+
const loaded = byId.get(event.id);
|
|
937
|
+
const sensor = loaded?.memory.frontmatter.sensor;
|
|
938
|
+
return {
|
|
939
|
+
at: event.at,
|
|
940
|
+
id: event.id,
|
|
941
|
+
title: titleFromMemory(loaded) || event.id,
|
|
942
|
+
source: event.source,
|
|
943
|
+
kind: event.kind ?? sensor?.kind ?? (event.source === "sensor" ? "regex" : null),
|
|
944
|
+
stage: event.stage ?? null,
|
|
945
|
+
exit_code: event.exit_code ?? null,
|
|
946
|
+
message: sensor?.message ?? null
|
|
947
|
+
};
|
|
948
|
+
}).sort((a, b) => b.at.localeCompare(a.at));
|
|
949
|
+
const preventedCountTotal = Object.values(usage.by_id).reduce((sum, item) => sum + item.prevented_count, 0);
|
|
950
|
+
return {
|
|
951
|
+
generated_at: now.toISOString(),
|
|
952
|
+
since: options.since.toISOString(),
|
|
953
|
+
window_days: Math.max(1, Math.round(windowMs / MS_PER_DAY2)),
|
|
954
|
+
total: rows.length,
|
|
955
|
+
previous_total: previousTotal,
|
|
956
|
+
prevented_count_total: preventedCountTotal,
|
|
957
|
+
trend: computePreventionTrend(events, now),
|
|
958
|
+
events: rows
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function renderPreventionReceipt(receipt) {
|
|
962
|
+
const lines = [
|
|
963
|
+
`Hivelore prevention receipt \u2014 last ${receipt.window_days} days`,
|
|
964
|
+
` ${receipt.total} repeat mistake${receipt.total === 1 ? "" : "s"} refused before ${receipt.total === 1 ? "it" : "they"} reached review.`
|
|
965
|
+
];
|
|
966
|
+
for (const row of receipt.events) {
|
|
967
|
+
const kind = row.kind ? `${row.kind} sensor` : row.source;
|
|
968
|
+
const exit = row.exit_code === null ? "" : `, exit ${row.exit_code}`;
|
|
969
|
+
const stage = row.stage ? ` \u2014 caught at ${row.stage}` : "";
|
|
970
|
+
lines.push(` \u2717\u2192\u2713 ${row.at.slice(0, 10)} ${row.id.padEnd(32)} (${kind}${exit}${stage})`);
|
|
971
|
+
}
|
|
972
|
+
lines.push(
|
|
973
|
+
` Trend: ${receipt.total} this window vs ${receipt.previous_total} previous window (${receipt.total <= receipt.previous_total ? "recurrences declining" : "recurrences rising"}).`
|
|
974
|
+
);
|
|
975
|
+
return lines.join("\n");
|
|
976
|
+
}
|
|
913
977
|
async function loadPreventionEvents(paths) {
|
|
914
978
|
const file = preventionLogPath(paths);
|
|
915
979
|
if (!existsSync4(file)) return [];
|
|
@@ -3782,10 +3846,10 @@ function sensorPatternBrittleness(pattern) {
|
|
|
3782
3846
|
function normalizeProjectPath(value) {
|
|
3783
3847
|
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^[ab]\//, "").replace(/\/+$/g, "");
|
|
3784
3848
|
}
|
|
3785
|
-
function sensorAppliesToPath(sensor, anchorPaths,
|
|
3849
|
+
function sensorAppliesToPath(sensor, anchorPaths, path20) {
|
|
3786
3850
|
const scopes = sensor.paths.length > 0 ? sensor.paths : anchorPaths;
|
|
3787
3851
|
if (scopes.length === 0) return true;
|
|
3788
|
-
const target = normalizeProjectPath(
|
|
3852
|
+
const target = normalizeProjectPath(path20);
|
|
3789
3853
|
return scopes.some((rawScope) => {
|
|
3790
3854
|
const scope = normalizeProjectPath(rawScope);
|
|
3791
3855
|
if (!scope) return false;
|
|
@@ -3978,6 +4042,154 @@ function addedLinesFromDiff(diff) {
|
|
|
3978
4042
|
return diff.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).map((l) => l.slice(1)).join("\n");
|
|
3979
4043
|
}
|
|
3980
4044
|
|
|
4045
|
+
// src/sensor-ledger.ts
|
|
4046
|
+
import { createHash as createHash2 } from "crypto";
|
|
4047
|
+
import { existsSync as existsSync15, readFileSync as readFileSync2 } from "fs";
|
|
4048
|
+
import { appendFile as appendFile4, mkdir as mkdir11, readFile as readFile14, rename, writeFile as writeFile9 } from "fs/promises";
|
|
4049
|
+
import path17 from "path";
|
|
4050
|
+
var MAX_LINES = 1e4;
|
|
4051
|
+
var RETAINED_LINES = 8e3;
|
|
4052
|
+
var DAY_MS = 864e5;
|
|
4053
|
+
function sensorLedgerPath(paths) {
|
|
4054
|
+
return path17.join(paths.runtimeDir, "enforcement", "sensor-ledger.ndjson");
|
|
4055
|
+
}
|
|
4056
|
+
function isEvaluation(value) {
|
|
4057
|
+
if (!value || typeof value !== "object") return false;
|
|
4058
|
+
const v = value;
|
|
4059
|
+
return typeof v.at === "string" && typeof v.memory_id === "string" && (v.kind === "regex" || v.kind === "shell" || v.kind === "test") && (v.stage === "pre-commit" || v.stage === "pre-push" || v.stage === "ci" || v.stage === "manual") && typeof v.head_sha === "string" && typeof v.scope_hash === "string" && (v.outcome === "fired" || v.outcome === "silent" || v.outcome === "unrunnable");
|
|
4060
|
+
}
|
|
4061
|
+
async function appendSensorEvaluations(paths, evaluations) {
|
|
4062
|
+
if (evaluations.length === 0) return;
|
|
4063
|
+
try {
|
|
4064
|
+
const file = sensorLedgerPath(paths);
|
|
4065
|
+
await mkdir11(path17.dirname(file), { recursive: true });
|
|
4066
|
+
await appendFile4(file, evaluations.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf8");
|
|
4067
|
+
const raw = await readFile14(file, "utf8");
|
|
4068
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
4069
|
+
if (lines.length > MAX_LINES) {
|
|
4070
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
4071
|
+
await writeFile9(temp, lines.slice(-RETAINED_LINES).join("\n") + "\n", "utf8");
|
|
4072
|
+
await rename(temp, file);
|
|
4073
|
+
}
|
|
4074
|
+
} catch {
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
async function loadSensorLedger(paths, opts = {}) {
|
|
4078
|
+
try {
|
|
4079
|
+
const file = sensorLedgerPath(paths);
|
|
4080
|
+
if (!existsSync15(file)) return [];
|
|
4081
|
+
const since = opts.since ? Date.parse(opts.since) : Number.NEGATIVE_INFINITY;
|
|
4082
|
+
const raw = await readFile14(file, "utf8");
|
|
4083
|
+
const out = [];
|
|
4084
|
+
for (const line of raw.split("\n")) {
|
|
4085
|
+
if (!line.trim()) continue;
|
|
4086
|
+
try {
|
|
4087
|
+
const parsed = JSON.parse(line);
|
|
4088
|
+
if (!isEvaluation(parsed)) continue;
|
|
4089
|
+
const at = Date.parse(parsed.at);
|
|
4090
|
+
if (!Number.isFinite(at) || at < since) continue;
|
|
4091
|
+
out.push(parsed);
|
|
4092
|
+
} catch {
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
return out;
|
|
4096
|
+
} catch {
|
|
4097
|
+
return [];
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
function computeScopeHash(root, scopedFiles) {
|
|
4101
|
+
try {
|
|
4102
|
+
const files = [...new Set(scopedFiles.map((f) => f.replace(/\\/g, "/")))].sort();
|
|
4103
|
+
if (files.length === 0) return "";
|
|
4104
|
+
const hash = createHash2("sha256");
|
|
4105
|
+
let included = 0;
|
|
4106
|
+
for (const rel of files) {
|
|
4107
|
+
const abs = path17.resolve(root, rel);
|
|
4108
|
+
if (!existsSync15(abs)) continue;
|
|
4109
|
+
try {
|
|
4110
|
+
hash.update(rel);
|
|
4111
|
+
hash.update("\0");
|
|
4112
|
+
hash.update(readFileSync2(abs));
|
|
4113
|
+
hash.update("\0");
|
|
4114
|
+
included++;
|
|
4115
|
+
} catch {
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
return included === 0 ? "" : hash.digest("hex");
|
|
4119
|
+
} catch {
|
|
4120
|
+
return "";
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
function assessSensorHealth(evaluations, now = /* @__PURE__ */ new Date(), opts = {}) {
|
|
4124
|
+
const cutoff = now.getTime() - 30 * DAY_MS;
|
|
4125
|
+
const byMemory = /* @__PURE__ */ new Map();
|
|
4126
|
+
for (const e of evaluations) {
|
|
4127
|
+
if (e.memory_id === "__gate__" || e.kind !== "shell" && e.kind !== "test") continue;
|
|
4128
|
+
const promotedAtIso = opts.promotedAt?.get(e.memory_id);
|
|
4129
|
+
if (promotedAtIso) {
|
|
4130
|
+
const promoted = Date.parse(promotedAtIso);
|
|
4131
|
+
if (Number.isFinite(promoted) && Date.parse(e.at) <= promoted) continue;
|
|
4132
|
+
}
|
|
4133
|
+
const list = byMemory.get(e.memory_id) ?? [];
|
|
4134
|
+
list.push(e);
|
|
4135
|
+
byMemory.set(e.memory_id, list);
|
|
4136
|
+
}
|
|
4137
|
+
const out = [];
|
|
4138
|
+
for (const [memoryId, all] of byMemory) {
|
|
4139
|
+
all.sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
4140
|
+
const recent = all.filter((e) => Date.parse(e.at) >= cutoff && Date.parse(e.at) <= now.getTime());
|
|
4141
|
+
const byHash = /* @__PURE__ */ new Map();
|
|
4142
|
+
for (const e of recent) {
|
|
4143
|
+
if (e.outcome === "unrunnable") continue;
|
|
4144
|
+
const list = byHash.get(e.scope_hash) ?? [];
|
|
4145
|
+
list.push(e);
|
|
4146
|
+
byHash.set(e.scope_hash, list);
|
|
4147
|
+
}
|
|
4148
|
+
const flaps = [];
|
|
4149
|
+
for (const [scopeHash, rows] of byHash) {
|
|
4150
|
+
for (let i = 1; i < rows.length; i++) {
|
|
4151
|
+
const previous = rows[i - 1];
|
|
4152
|
+
const current = rows[i];
|
|
4153
|
+
if (previous.outcome !== current.outcome) {
|
|
4154
|
+
flaps.push({ memory_id: memoryId, scope_hash: scopeHash, previous, current });
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
flaps.sort((a, b) => Date.parse(a.current.at) - Date.parse(b.current.at));
|
|
4159
|
+
const runnable = all.filter((e) => e.outcome !== "unrunnable");
|
|
4160
|
+
const span = runnable.length > 1 ? Date.parse(runnable[runnable.length - 1].at) - Date.parse(runnable[0].at) : 0;
|
|
4161
|
+
out.push({
|
|
4162
|
+
memory_id: memoryId,
|
|
4163
|
+
flap_count: flaps.length,
|
|
4164
|
+
flaps,
|
|
4165
|
+
quarantine_pending: flaps.length >= 2,
|
|
4166
|
+
never_fired: runnable.length >= 20 && span >= 30 * DAY_MS && runnable.every((e) => e.outcome === "silent"),
|
|
4167
|
+
evaluation_count: runnable.length
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
return out.sort((a, b) => a.memory_id.localeCompare(b.memory_id));
|
|
4171
|
+
}
|
|
4172
|
+
function sensorPromotedAtMap(frontmatters) {
|
|
4173
|
+
const out = /* @__PURE__ */ new Map();
|
|
4174
|
+
for (const fm of frontmatters) {
|
|
4175
|
+
if (fm.sensor?.promoted_at) out.set(fm.id, fm.sensor.promoted_at);
|
|
4176
|
+
}
|
|
4177
|
+
return out;
|
|
4178
|
+
}
|
|
4179
|
+
function quarantineNote(at, flapCount) {
|
|
4180
|
+
return `> Quarantined ${at}: oracle flapped ${flapCount}\xD7 on identical inputs \u2014 demoted block\u2192warn. Fix the test, then re-promote with \`hivelore sensors promote <id>\`.`;
|
|
4181
|
+
}
|
|
4182
|
+
function withQuarantineNote(body, at, flapCount) {
|
|
4183
|
+
const without = body.split("\n").filter((line) => !line.startsWith("> Quarantined ")).join("\n").trimEnd();
|
|
4184
|
+
return `${without}
|
|
4185
|
+
|
|
4186
|
+
${quarantineNote(at, flapCount)}
|
|
4187
|
+
`;
|
|
4188
|
+
}
|
|
4189
|
+
function withoutQuarantineNote(body) {
|
|
4190
|
+
return body.split("\n").filter((line) => !line.startsWith("> Quarantined ")).join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
4191
|
+
}
|
|
4192
|
+
|
|
3981
4193
|
// src/sensor-suggest.ts
|
|
3982
4194
|
var CODE_TOKEN_RE = /`([^`\n]{3,80})`|["']([A-Za-z0-9_.:-]{3,80})["']|\b([A-Za-z][A-Za-z0-9_.:-]{2,79})\b/g;
|
|
3983
4195
|
var SENSOR_STOPWORDS = /* @__PURE__ */ new Set([
|
|
@@ -4425,8 +4637,8 @@ function normalizeFindingSeverity(raw) {
|
|
|
4425
4637
|
return "info";
|
|
4426
4638
|
}
|
|
4427
4639
|
}
|
|
4428
|
-
function findingKey(tool, ruleId,
|
|
4429
|
-
return `${tool}:${ruleId}:${
|
|
4640
|
+
function findingKey(tool, ruleId, path20) {
|
|
4641
|
+
return `${tool}:${ruleId}:${path20}`;
|
|
4430
4642
|
}
|
|
4431
4643
|
function coerceJson(input) {
|
|
4432
4644
|
if (typeof input === "string") {
|
|
@@ -4460,8 +4672,8 @@ function parseSarif(input) {
|
|
|
4460
4672
|
const physical = asRecord(location.physicalLocation);
|
|
4461
4673
|
const artifact = asRecord(physical.artifactLocation);
|
|
4462
4674
|
const region = asRecord(physical.region);
|
|
4463
|
-
const
|
|
4464
|
-
if (!
|
|
4675
|
+
const path20 = typeof artifact.uri === "string" ? normalizeUri(artifact.uri) : "";
|
|
4676
|
+
if (!path20) continue;
|
|
4465
4677
|
const line = typeof region.startLine === "number" ? region.startLine : void 0;
|
|
4466
4678
|
const snippet = typeof asRecord(region.snippet).text === "string" ? asRecord(region.snippet).text.trim() : void 0;
|
|
4467
4679
|
findings.push({
|
|
@@ -4469,10 +4681,10 @@ function parseSarif(input) {
|
|
|
4469
4681
|
ruleId,
|
|
4470
4682
|
message: message.trim(),
|
|
4471
4683
|
severity,
|
|
4472
|
-
path:
|
|
4684
|
+
path: path20,
|
|
4473
4685
|
...line !== void 0 ? { line } : {},
|
|
4474
4686
|
...snippet ? { snippet } : {},
|
|
4475
|
-
key: findingKey(tool, ruleId,
|
|
4687
|
+
key: findingKey(tool, ruleId, path20)
|
|
4476
4688
|
});
|
|
4477
4689
|
}
|
|
4478
4690
|
}
|
|
@@ -4491,17 +4703,17 @@ function parseSonar(input) {
|
|
|
4491
4703
|
(typeof issue.severity === "string" ? issue.severity : void 0) ?? impactSeverity
|
|
4492
4704
|
);
|
|
4493
4705
|
const component = typeof issue.component === "string" ? issue.component : "";
|
|
4494
|
-
const
|
|
4495
|
-
if (!
|
|
4706
|
+
const path20 = componentToPath(component);
|
|
4707
|
+
if (!path20) continue;
|
|
4496
4708
|
const line = typeof issue.line === "number" ? issue.line : void 0;
|
|
4497
4709
|
findings.push({
|
|
4498
4710
|
tool: "sonar",
|
|
4499
4711
|
ruleId,
|
|
4500
4712
|
message,
|
|
4501
4713
|
severity,
|
|
4502
|
-
path:
|
|
4714
|
+
path: path20,
|
|
4503
4715
|
...line !== void 0 ? { line } : {},
|
|
4504
|
-
key: findingKey("sonar", ruleId,
|
|
4716
|
+
key: findingKey("sonar", ruleId, path20)
|
|
4505
4717
|
});
|
|
4506
4718
|
}
|
|
4507
4719
|
return findings;
|
|
@@ -4514,7 +4726,7 @@ function parseEslintJson(input, opts = {}) {
|
|
|
4514
4726
|
const file = asRecord(fileRaw);
|
|
4515
4727
|
const rawPath = typeof file.filePath === "string" ? file.filePath : "";
|
|
4516
4728
|
if (!rawPath) continue;
|
|
4517
|
-
const
|
|
4729
|
+
const path20 = cwd && rawPath.startsWith(cwd) ? rawPath.slice(cwd.length) : rawPath;
|
|
4518
4730
|
for (const msgRaw of asArray(file.messages)) {
|
|
4519
4731
|
const msg = asRecord(msgRaw);
|
|
4520
4732
|
const ruleId = typeof msg.ruleId === "string" && msg.ruleId ? msg.ruleId : "parse-error";
|
|
@@ -4526,9 +4738,9 @@ function parseEslintJson(input, opts = {}) {
|
|
|
4526
4738
|
ruleId,
|
|
4527
4739
|
message,
|
|
4528
4740
|
severity,
|
|
4529
|
-
path:
|
|
4741
|
+
path: path20,
|
|
4530
4742
|
...line !== void 0 ? { line } : {},
|
|
4531
|
-
key: findingKey("eslint", ruleId,
|
|
4743
|
+
key: findingKey("eslint", ruleId, path20)
|
|
4532
4744
|
});
|
|
4533
4745
|
}
|
|
4534
4746
|
}
|
|
@@ -4946,7 +5158,7 @@ function tallyHotFiles(paths, source = "agent") {
|
|
|
4946
5158
|
if (!norm) continue;
|
|
4947
5159
|
counts.set(norm, (counts.get(norm) ?? 0) + 1);
|
|
4948
5160
|
}
|
|
4949
|
-
return [...counts.entries()].map(([
|
|
5161
|
+
return [...counts.entries()].map(([path20, changes]) => ({ path: path20, changes, source })).sort((a, b) => b.changes - a.changes);
|
|
4950
5162
|
}
|
|
4951
5163
|
function mergeHotFiles(a, b) {
|
|
4952
5164
|
const merged = /* @__PURE__ */ new Map();
|
|
@@ -4966,21 +5178,21 @@ function mergeHotFiles(a, b) {
|
|
|
4966
5178
|
}
|
|
4967
5179
|
|
|
4968
5180
|
// src/eval-history.ts
|
|
4969
|
-
import { appendFile as
|
|
4970
|
-
import { existsSync as
|
|
4971
|
-
import
|
|
5181
|
+
import { appendFile as appendFile5, mkdir as mkdir12, readFile as readFile15 } from "fs/promises";
|
|
5182
|
+
import { existsSync as existsSync16 } from "fs";
|
|
5183
|
+
import path18 from "path";
|
|
4972
5184
|
function evalHistoryPath(paths) {
|
|
4973
|
-
return
|
|
5185
|
+
return path18.join(paths.haiveDir, ".cache", "eval-history.jsonl");
|
|
4974
5186
|
}
|
|
4975
5187
|
async function appendEvalHistory(paths, entry) {
|
|
4976
5188
|
const file = evalHistoryPath(paths);
|
|
4977
|
-
await
|
|
4978
|
-
await
|
|
5189
|
+
await mkdir12(path18.dirname(file), { recursive: true });
|
|
5190
|
+
await appendFile5(file, JSON.stringify(entry) + "\n", "utf8");
|
|
4979
5191
|
}
|
|
4980
5192
|
async function loadEvalHistory(paths) {
|
|
4981
5193
|
const file = evalHistoryPath(paths);
|
|
4982
|
-
if (!
|
|
4983
|
-
const raw = await
|
|
5194
|
+
if (!existsSync16(file)) return [];
|
|
5195
|
+
const raw = await readFile15(file, "utf8").catch(() => "");
|
|
4984
5196
|
const out = [];
|
|
4985
5197
|
for (const line of raw.split("\n")) {
|
|
4986
5198
|
const trimmed = line.trim();
|
|
@@ -5124,6 +5336,81 @@ function proposeSeedsFromCommits(commits, limit = 20) {
|
|
|
5124
5336
|
return out;
|
|
5125
5337
|
}
|
|
5126
5338
|
|
|
5339
|
+
// src/gate-miss.ts
|
|
5340
|
+
function planGitWatch(state, headSha) {
|
|
5341
|
+
const next = { last_scanned_sha: headSha };
|
|
5342
|
+
if (!state?.last_scanned_sha) return { action: "initialize", next };
|
|
5343
|
+
if (state.last_scanned_sha === headSha) return { action: "idle", next };
|
|
5344
|
+
return { action: "scan", range: `${state.last_scanned_sha}..${headSha}`, next };
|
|
5345
|
+
}
|
|
5346
|
+
var REVERTED_SHA_RE = /\bThis reverts commit ([0-9a-f]{7,40})\b/i;
|
|
5347
|
+
var BODY_REVERTED_SHA_RE = /^Reverted SHA:\s*([0-9a-f]{7,40})\s*$/im;
|
|
5348
|
+
function revertedShaFromCommit(commit) {
|
|
5349
|
+
return commit.reverted_sha ?? REVERTED_SHA_RE.exec(commit.body ?? "")?.[1] ?? null;
|
|
5350
|
+
}
|
|
5351
|
+
function existingGateMissShas(memories) {
|
|
5352
|
+
const shas = /* @__PURE__ */ new Set();
|
|
5353
|
+
for (const loaded of memories) {
|
|
5354
|
+
if (!loaded.memory.frontmatter.tags.includes("gate-miss")) continue;
|
|
5355
|
+
const sha = BODY_REVERTED_SHA_RE.exec(loaded.memory.body)?.[1];
|
|
5356
|
+
if (sha) shas.add(sha);
|
|
5357
|
+
}
|
|
5358
|
+
return shas;
|
|
5359
|
+
}
|
|
5360
|
+
function gatePassedShas(evaluations) {
|
|
5361
|
+
return new Set(
|
|
5362
|
+
evaluations.filter((e) => e.memory_id === "__gate__" && e.outcome === "silent" && e.head_sha).map((e) => e.head_sha)
|
|
5363
|
+
);
|
|
5364
|
+
}
|
|
5365
|
+
function shaMatches(set, sha) {
|
|
5366
|
+
for (const candidate of set) {
|
|
5367
|
+
if (candidate === sha || candidate.startsWith(sha) || sha.startsWith(candidate)) return true;
|
|
5368
|
+
}
|
|
5369
|
+
return false;
|
|
5370
|
+
}
|
|
5371
|
+
function proposeGateMissDrafts(commits, existingRevertedShas, passedShas, opts = {}) {
|
|
5372
|
+
const seeds = proposeSeedsFromCommits(commits, commits.length);
|
|
5373
|
+
const bySha = new Map(commits.map((commit) => [commit.sha, commit]));
|
|
5374
|
+
const seen = new Set(existingRevertedShas);
|
|
5375
|
+
const out = [];
|
|
5376
|
+
for (const seed of seeds) {
|
|
5377
|
+
const commit = bySha.get(seed.source_sha);
|
|
5378
|
+
if (!commit) continue;
|
|
5379
|
+
const failedSha = revertedShaFromCommit(commit) ?? commit.sha;
|
|
5380
|
+
if (shaMatches(seen, failedSha)) continue;
|
|
5381
|
+
seen.add(failedSha);
|
|
5382
|
+
const gatePassed = shaMatches(passedShas, failedSha);
|
|
5383
|
+
const paths = (commit.files ?? []).filter((p) => !p.startsWith(".ai/")).filter((p) => opts.pathExists?.(p) ?? true).slice(0, 8);
|
|
5384
|
+
const base = `# Gate miss: ${seed.what}
|
|
5385
|
+
|
|
5386
|
+
A git ${seed.kind} indicates that a change escaped the existing harness. This is a proposed lesson only; review the actual regression before validating it.
|
|
5387
|
+
|
|
5388
|
+
Reverted SHA: ${failedSha}
|
|
5389
|
+
Revert SHA: ${commit.sha}
|
|
5390
|
+
Subject: ${seed.what}
|
|
5391
|
+
` + (paths.length > 0 ? `Top paths: ${paths.join(", ")}
|
|
5392
|
+
` : "") + `
|
|
5393
|
+
**Why it failed / do NOT use:** ${seed.why_failed}
|
|
5394
|
+
`;
|
|
5395
|
+
const gateLine = gatePassed ? "\nThe gate PASSED this commit \u2014 a validated sensor here upgrades the harness.\n" : "";
|
|
5396
|
+
const candidate = suggestSensorSeed(seed.what, paths);
|
|
5397
|
+
const sensorHint = candidate ? `
|
|
5398
|
+
proposed_sensor_seed: ${JSON.stringify(candidate)}
|
|
5399
|
+
` : "\nproposed_sensor_seed: inspect the revert diff, then author a deterministic candidate with `hivelore sensors propose <id>`.\n";
|
|
5400
|
+
out.push({
|
|
5401
|
+
slug: `gate-miss-${failedSha.slice(0, 12)}`,
|
|
5402
|
+
reverted_sha: failedSha,
|
|
5403
|
+
revert_sha: commit.sha,
|
|
5404
|
+
subject: seed.what,
|
|
5405
|
+
paths,
|
|
5406
|
+
kind: seed.kind,
|
|
5407
|
+
gate_passed: gatePassed,
|
|
5408
|
+
body: base + gateLine + sensorHint
|
|
5409
|
+
});
|
|
5410
|
+
}
|
|
5411
|
+
return out;
|
|
5412
|
+
}
|
|
5413
|
+
|
|
5127
5414
|
// src/seed.ts
|
|
5128
5415
|
var JS_DETECTORS = [
|
|
5129
5416
|
["nestjs", ["@nestjs/core"]],
|
|
@@ -5248,12 +5535,12 @@ ${trimmed}`;
|
|
|
5248
5535
|
}
|
|
5249
5536
|
|
|
5250
5537
|
// src/handoff.ts
|
|
5251
|
-
import { writeFile as
|
|
5252
|
-
import { existsSync as
|
|
5253
|
-
import
|
|
5538
|
+
import { writeFile as writeFile10, readFile as readFile16, stat as stat3 } from "fs/promises";
|
|
5539
|
+
import { existsSync as existsSync17 } from "fs";
|
|
5540
|
+
import path19 from "path";
|
|
5254
5541
|
var HANDOFF_FILENAME = "NEXT.md";
|
|
5255
5542
|
function handoffFilePath(root) {
|
|
5256
|
-
return
|
|
5543
|
+
return path19.join(root, HANDOFF_FILENAME);
|
|
5257
5544
|
}
|
|
5258
5545
|
function buildHandoffMarkdown(data) {
|
|
5259
5546
|
const at = (data.at ?? /* @__PURE__ */ new Date()).toISOString();
|
|
@@ -5302,18 +5589,18 @@ function buildHandoffMarkdown(data) {
|
|
|
5302
5589
|
}
|
|
5303
5590
|
async function writeSessionHandoff(root, data) {
|
|
5304
5591
|
const file = handoffFilePath(root);
|
|
5305
|
-
await
|
|
5592
|
+
await writeFile10(file, buildHandoffMarkdown(data), "utf8");
|
|
5306
5593
|
return file;
|
|
5307
5594
|
}
|
|
5308
5595
|
async function readSessionHandoff(root) {
|
|
5309
5596
|
const file = handoffFilePath(root);
|
|
5310
|
-
if (!
|
|
5311
|
-
const raw = await
|
|
5597
|
+
if (!existsSync17(file)) return null;
|
|
5598
|
+
const raw = await readFile16(file, "utf8").catch(() => "");
|
|
5312
5599
|
return raw.trim() ? raw : null;
|
|
5313
5600
|
}
|
|
5314
5601
|
async function handoffAgeMs(root, now = /* @__PURE__ */ new Date()) {
|
|
5315
5602
|
const file = handoffFilePath(root);
|
|
5316
|
-
if (!
|
|
5603
|
+
if (!existsSync17(file)) return null;
|
|
5317
5604
|
try {
|
|
5318
5605
|
const s = await stat3(file);
|
|
5319
5606
|
return Math.max(0, now.getTime() - s.mtimeMs);
|
|
@@ -5429,10 +5716,12 @@ export {
|
|
|
5429
5716
|
appendEvalHistory,
|
|
5430
5717
|
appendPreventionEvent,
|
|
5431
5718
|
appendRuntimeJournalEntry,
|
|
5719
|
+
appendSensorEvaluations,
|
|
5432
5720
|
appendUsageEvent,
|
|
5433
5721
|
applyConflictResolution,
|
|
5434
5722
|
applyFeedbackAdjustment,
|
|
5435
5723
|
assessBootstrapState,
|
|
5724
|
+
assessSensorHealth,
|
|
5436
5725
|
bridgeMemorySummary,
|
|
5437
5726
|
briefingMarkerPath,
|
|
5438
5727
|
briefingMarkersDir,
|
|
@@ -5443,6 +5732,7 @@ export {
|
|
|
5443
5732
|
buildDocFrequency,
|
|
5444
5733
|
buildFrontmatter,
|
|
5445
5734
|
buildHandoffMarkdown,
|
|
5735
|
+
buildPreventionReceipt,
|
|
5446
5736
|
buildReport,
|
|
5447
5737
|
bumpRead,
|
|
5448
5738
|
classifyMemoryPriority,
|
|
@@ -5459,6 +5749,7 @@ export {
|
|
|
5459
5749
|
computeImpact,
|
|
5460
5750
|
computePreventionTrend,
|
|
5461
5751
|
computeRecurrence,
|
|
5752
|
+
computeScopeHash,
|
|
5462
5753
|
configPath,
|
|
5463
5754
|
contractLockPath,
|
|
5464
5755
|
countSourceFilesOnDisk,
|
|
@@ -5475,6 +5766,7 @@ export {
|
|
|
5475
5766
|
estimateTokens,
|
|
5476
5767
|
evalHistoryPath,
|
|
5477
5768
|
evaluateSkillActivation,
|
|
5769
|
+
existingGateMissShas,
|
|
5478
5770
|
extractActionsBriefBody,
|
|
5479
5771
|
extractReferencedPaths,
|
|
5480
5772
|
extractSensorExamples,
|
|
@@ -5488,6 +5780,7 @@ export {
|
|
|
5488
5780
|
findingBody,
|
|
5489
5781
|
findingToDraft,
|
|
5490
5782
|
firstMemoryOneLine,
|
|
5783
|
+
gatePassedShas,
|
|
5491
5784
|
generateBridges,
|
|
5492
5785
|
getUsage,
|
|
5493
5786
|
globToRegExp,
|
|
@@ -5525,6 +5818,7 @@ export {
|
|
|
5525
5818
|
loadMemoriesFromDirDetailed,
|
|
5526
5819
|
loadMemory,
|
|
5527
5820
|
loadPreventionEvents,
|
|
5821
|
+
loadSensorLedger,
|
|
5528
5822
|
loadUsageIndex,
|
|
5529
5823
|
looksLikeGenericAdvice,
|
|
5530
5824
|
meetsSeedQualityFloor,
|
|
@@ -5549,13 +5843,16 @@ export {
|
|
|
5549
5843
|
pathsOverlap,
|
|
5550
5844
|
pickSnippetNeedle,
|
|
5551
5845
|
planConflictResolution,
|
|
5846
|
+
planGitWatch,
|
|
5552
5847
|
prepareBridgeData,
|
|
5553
5848
|
preventionLogPath,
|
|
5554
5849
|
priorityRank,
|
|
5555
5850
|
prioritySignals,
|
|
5556
5851
|
projectContextRecentlyEmitted,
|
|
5852
|
+
proposeGateMissDrafts,
|
|
5557
5853
|
proposeSeedsFromCommits,
|
|
5558
5854
|
pullCrossRepoSources,
|
|
5855
|
+
quarantineNote,
|
|
5559
5856
|
queryCodeMap,
|
|
5560
5857
|
rankMemoriesLexical,
|
|
5561
5858
|
readRecentBriefingMarker,
|
|
@@ -5571,11 +5868,13 @@ export {
|
|
|
5571
5868
|
relPathFrom,
|
|
5572
5869
|
renderBootstrapChecklist,
|
|
5573
5870
|
renderCaughtForYou,
|
|
5871
|
+
renderPreventionReceipt,
|
|
5574
5872
|
resolveBriefingBudget,
|
|
5575
5873
|
resolveHaivePaths,
|
|
5576
5874
|
resolveManifestFiles,
|
|
5577
5875
|
resolveProjectInfo,
|
|
5578
5876
|
retirementSignal,
|
|
5877
|
+
revertedShaFromCommit,
|
|
5579
5878
|
runRegexSensor,
|
|
5580
5879
|
runSensors,
|
|
5581
5880
|
runtimeJournalPath,
|
|
@@ -5587,7 +5886,9 @@ export {
|
|
|
5587
5886
|
scoreSensorCase,
|
|
5588
5887
|
selectCommandSensors,
|
|
5589
5888
|
sensorAppliesToPath,
|
|
5889
|
+
sensorLedgerPath,
|
|
5590
5890
|
sensorPatternBrittleness,
|
|
5891
|
+
sensorPromotedAtMap,
|
|
5591
5892
|
sensorSelfCheck,
|
|
5592
5893
|
sensorTargetsFromDiff,
|
|
5593
5894
|
serializeMemory,
|
|
@@ -5613,6 +5914,8 @@ export {
|
|
|
5613
5914
|
usagePath,
|
|
5614
5915
|
verifyAnchor,
|
|
5615
5916
|
watchContracts,
|
|
5917
|
+
withQuarantineNote,
|
|
5918
|
+
withoutQuarantineNote,
|
|
5616
5919
|
writeBriefingMarker,
|
|
5617
5920
|
writeSessionHandoff
|
|
5618
5921
|
};
|