@merittdev/horus 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +118 -12
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -65880,7 +65880,7 @@ init_cjs_shims();
|
|
|
65880
65880
|
|
|
65881
65881
|
// ../../packages/core/src/version.ts
|
|
65882
65882
|
init_cjs_shims();
|
|
65883
|
-
var HORUS_VERSION = true ? "0.
|
|
65883
|
+
var HORUS_VERSION = true ? "0.15.0" : "dev";
|
|
65884
65884
|
var PINNED_SOURCE_VERSION = "2.0.2";
|
|
65885
65885
|
|
|
65886
65886
|
// ../../packages/core/src/evidence.ts
|
|
@@ -70856,6 +70856,9 @@ init_cjs_shims();
|
|
|
70856
70856
|
|
|
70857
70857
|
// ../../packages/connectors/src/source/client.ts
|
|
70858
70858
|
init_cjs_shims();
|
|
70859
|
+
function encodeNodePath(nodeId) {
|
|
70860
|
+
return encodeURI(nodeId).replace(/#/g, "%23").replace(/\?/g, "%3F");
|
|
70861
|
+
}
|
|
70859
70862
|
var SourceHttpError = class extends Error {
|
|
70860
70863
|
status;
|
|
70861
70864
|
body;
|
|
@@ -70982,7 +70985,7 @@ var SourceHttpClient = class {
|
|
|
70982
70985
|
impact(nodeId, depth = 3) {
|
|
70983
70986
|
return this.request(
|
|
70984
70987
|
"GET",
|
|
70985
|
-
`/api/impact/${
|
|
70988
|
+
`/api/impact/${encodeNodePath(nodeId)}?depth=${depth}`
|
|
70986
70989
|
);
|
|
70987
70990
|
}
|
|
70988
70991
|
diff(base2, compare) {
|
|
@@ -71032,7 +71035,7 @@ var SourceHttpClient = class {
|
|
|
71032
71035
|
}
|
|
71033
71036
|
/** Process flows a symbol participates in, with each flow's named ordered steps. */
|
|
71034
71037
|
flows(nodeId) {
|
|
71035
|
-
return this.request("GET", `/api/flows/${
|
|
71038
|
+
return this.request("GET", `/api/flows/${encodeNodePath(nodeId)}`);
|
|
71036
71039
|
}
|
|
71037
71040
|
/** Method symbols of `className` defined in `file`, ordered by start line. */
|
|
71038
71041
|
async classMethods(file, className) {
|
|
@@ -71044,7 +71047,7 @@ var SourceHttpClient = class {
|
|
|
71044
71047
|
}
|
|
71045
71048
|
/** Extended node detail — node + content + callers/callees/typeRefs + imports + coupling + communities. */
|
|
71046
71049
|
node(nodeId) {
|
|
71047
|
-
return this.request("GET", `/api/node/${
|
|
71050
|
+
return this.request("GET", `/api/node/${encodeNodePath(nodeId)}`);
|
|
71048
71051
|
}
|
|
71049
71052
|
/** Community clusters with their member nodes (source-graph extraction). */
|
|
71050
71053
|
async communities() {
|
|
@@ -93577,6 +93580,94 @@ function selectHeadlineCause(rankedCauses, isLinkedToSeed, rerank) {
|
|
|
93577
93580
|
return { topCause, topLinkedCause, headlineCause, headlineLinked };
|
|
93578
93581
|
}
|
|
93579
93582
|
|
|
93583
|
+
// ../../packages/engine/src/detect-data-flow-cause.ts
|
|
93584
|
+
init_cjs_shims();
|
|
93585
|
+
function stripComments(s5) {
|
|
93586
|
+
return s5.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
93587
|
+
}
|
|
93588
|
+
function grp(m5, i3) {
|
|
93589
|
+
return m5[i3] ?? "";
|
|
93590
|
+
}
|
|
93591
|
+
function hedged(symbolName, clause) {
|
|
93592
|
+
return `${symbolName} could explain this \u2014 it ${clause}. Source-only signal: verify against runtime evidence.`;
|
|
93593
|
+
}
|
|
93594
|
+
var CONFIG_SRC = /process\.env|config\.|opts\.|options\.|this\.|props\.|import\.meta/;
|
|
93595
|
+
function detectFixedCadence(body2, name2) {
|
|
93596
|
+
const timer2 = /\bset(?:Interval|Timeout)\s*\(\s*[^,]+,\s*([0-9_]{3,})\s*\)/.exec(body2);
|
|
93597
|
+
if (timer2) {
|
|
93598
|
+
const frag = grp(timer2, 0).trim().slice(0, 48);
|
|
93599
|
+
return { pattern: "fixed-cadence", matched: frag, baseScore: 0.2, title: hedged(name2, `polls/schedules on a fixed interval (\`${frag}\`), so data freshness is bounded by that cadence rather than the event`) };
|
|
93600
|
+
}
|
|
93601
|
+
const cron = /(['"`])(?:\*|[0-9,*/-]+)(?:\s+[0-9,*/-]+){4}\1/.exec(body2);
|
|
93602
|
+
if (cron) {
|
|
93603
|
+
const frag = grp(cron, 0);
|
|
93604
|
+
return { pattern: "fixed-cadence", matched: frag.slice(0, 48), baseScore: 0.2, title: hedged(name2, `runs on a fixed cron cadence (\`${frag}\`), so an effect activates on the next tick, not at its target time`) };
|
|
93605
|
+
}
|
|
93606
|
+
const cad = /\b(?:pollInterval|refetchInterval|cadence|intervalMs|tickMs)\b\s*[:=]\s*[0-9_]{2,}/.exec(body2);
|
|
93607
|
+
if (cad && !CONFIG_SRC.test(grp(cad, 0))) {
|
|
93608
|
+
const frag = grp(cad, 0).trim().slice(0, 40);
|
|
93609
|
+
return { pattern: "fixed-cadence", matched: frag, baseScore: 0.2, title: hedged(name2, `uses a fixed cadence (\`${frag}\`); the symptom may track that interval`) };
|
|
93610
|
+
}
|
|
93611
|
+
return null;
|
|
93612
|
+
}
|
|
93613
|
+
function detectInPlaceMutation(body2, ctx) {
|
|
93614
|
+
const hay = `${ctx.symbol.name} ${ctx.symbol.filePath ?? ""} ${ctx.symbol.signature ?? ""}`;
|
|
93615
|
+
const contextual = /reduc|\bslice\b|store|mutat|\bstate\b/i.test(hay) || ctx.imports.some((i3) => /redux|zustand|immer|valtio|mobx/i.test(i3));
|
|
93616
|
+
if (!contextual) return null;
|
|
93617
|
+
if (/return\s*\{\s*\.\.\.|=>\s*\(?\s*\{\s*\.\.\.|\.\.\.(?:state|prev|draft)\b/.test(body2)) return null;
|
|
93618
|
+
const assign = /\b(state|store|draft|cache|acc|accumulator)(?:\.[A-Za-z0-9_]+)+\s*=(?!=)/.exec(body2);
|
|
93619
|
+
const method = /\b(state|store|draft|cache)(?:\.[A-Za-z0-9_]+)*\.(?:push|splice|sort|reverse|pop|shift|unshift)\s*\(/.exec(body2);
|
|
93620
|
+
const m5 = assign ?? method;
|
|
93621
|
+
if (!m5) return null;
|
|
93622
|
+
const obj = grp(m5, 1) || "state";
|
|
93623
|
+
const frag = grp(m5, 0).trim().slice(0, 40);
|
|
93624
|
+
return { pattern: "in-place-mutation", matched: frag, baseScore: 0.2, title: hedged(ctx.symbol.name, `appears to mutate the shared \`${obj}\` object in place (\`${frag}\`) rather than producing a new reference, so downstream readers may observe stale or partially-updated state`) };
|
|
93625
|
+
}
|
|
93626
|
+
function detectUnawaitedWrite(body2, ctx) {
|
|
93627
|
+
const isAsync2 = /\basync\b/.test(ctx.symbol.signature ?? "") || /\bawait\b/.test(body2);
|
|
93628
|
+
if (!isAsync2 || !/\bawait\b/.test(body2)) return null;
|
|
93629
|
+
const calleeNames = new Set(ctx.callees.map((c4) => c4.name));
|
|
93630
|
+
for (const line2 of body2.split("\n")) {
|
|
93631
|
+
const t2 = line2.trim();
|
|
93632
|
+
const call = /^([A-Za-z_$][\w.$]*)\s*\(/.exec(t2);
|
|
93633
|
+
if (!call) continue;
|
|
93634
|
+
if (/^(?:await|return|void|yield|const|let|var|if|for|while|}|else|switch|throw|case)\b/.test(t2)) continue;
|
|
93635
|
+
if (t2.includes(".then(") || t2.includes(".catch(") || t2.includes("await ")) continue;
|
|
93636
|
+
const callee = grp(call, 1);
|
|
93637
|
+
const fn2 = callee.split(".").pop() ?? "";
|
|
93638
|
+
if (/^(?:emit|log|console|track|debug|warn|error|info|print|notify|next|push|set[A-Z])/.test(fn2)) continue;
|
|
93639
|
+
const writeLike = /^(?:save|write|flush|persist|commit|sync|update|insert|upsert|fetch|load|send|post)/i.test(fn2) || /Async$/.test(fn2);
|
|
93640
|
+
if (!writeLike) continue;
|
|
93641
|
+
if (!calleeNames.has(callee) && !calleeNames.has(fn2)) continue;
|
|
93642
|
+
return { pattern: "unawaited-async-write", matched: t2.slice(0, 60), baseScore: 0.19, title: hedged(ctx.symbol.name, `calls \`${fn2}\` without \`await\` while awaiting elsewhere, so a later read may race ahead of that write`) };
|
|
93643
|
+
}
|
|
93644
|
+
return null;
|
|
93645
|
+
}
|
|
93646
|
+
function detectHardcodedBound(body2, name2) {
|
|
93647
|
+
const eq2 = /\b(Object\.is|shallowEqual|isEqual)\s*\(/.exec(body2);
|
|
93648
|
+
if (eq2) {
|
|
93649
|
+
return { pattern: "hardcoded-bound", matched: grp(eq2, 0).slice(0, 40), baseScore: 0.18, title: hedged(name2, `gates on a reference/shallow equality check (\`${grp(eq2, 1)}\`), so an in-place or structurally-equal update can be skipped and a change goes unobserved`) };
|
|
93650
|
+
}
|
|
93651
|
+
const re = /\b([A-Za-z_$][\w$]*)\s*(?:<=|>=|<|>|===|!==)\s*([0-9]+(?:\.[0-9]+)?)\b/g;
|
|
93652
|
+
for (let m5 = re.exec(body2); m5 !== null; m5 = re.exec(body2)) {
|
|
93653
|
+
const id = grp(m5, 1);
|
|
93654
|
+
const n4 = Number(grp(m5, 2));
|
|
93655
|
+
if (n4 === 0 || n4 === 1) continue;
|
|
93656
|
+
if (!/thresh|limit|max|min|retr|attempt|backoff|count|rate|ratio|ttl|timeout|delay|capacity|tolerance|distance|score/i.test(id)) continue;
|
|
93657
|
+
if (/\.length\b|\bidx\b|\bindex\b|\bi\b/.test(id)) continue;
|
|
93658
|
+
const frag = grp(m5, 0).trim().slice(0, 40);
|
|
93659
|
+
return { pattern: "hardcoded-bound", matched: frag, baseScore: 0.2, title: hedged(name2, `gates the flow on a fixed bound (\`${frag}\`), so behavior can flip at that constant independent of any deploy`) };
|
|
93660
|
+
}
|
|
93661
|
+
return null;
|
|
93662
|
+
}
|
|
93663
|
+
function detectDataFlowCause(ctx) {
|
|
93664
|
+
const raw = ctx.sourceBody ?? ctx.snippet ?? "";
|
|
93665
|
+
if (raw.length < 40) return null;
|
|
93666
|
+
const body2 = stripComments(raw);
|
|
93667
|
+
const nm = ctx.symbol.name;
|
|
93668
|
+
return detectFixedCadence(body2, nm) ?? detectInPlaceMutation(body2, ctx) ?? detectUnawaitedWrite(body2, ctx) ?? detectHardcodedBound(body2, nm);
|
|
93669
|
+
}
|
|
93670
|
+
|
|
93580
93671
|
// ../../packages/engine/src/hypotheses.ts
|
|
93581
93672
|
init_cjs_shims();
|
|
93582
93673
|
function generateHypotheses(evidence2, _correlation, ctx) {
|
|
@@ -96501,8 +96592,8 @@ async function investigate(input, deps) {
|
|
|
96501
96592
|
label = top.name === "constructor" ? `${(top.id.split(":").pop() ?? "").replace(/\.constructor$/, "") || top.name} (constructor)` : top.name;
|
|
96502
96593
|
[ctx, impact, flows] = await Promise.all([
|
|
96503
96594
|
code.context(top.id),
|
|
96504
|
-
code.impact(top.id, 2),
|
|
96505
|
-
code.flowsFor(top.id)
|
|
96595
|
+
code.impact(top.id, 2).catch(() => null),
|
|
96596
|
+
code.flowsFor(top.id).catch(() => [])
|
|
96506
96597
|
]);
|
|
96507
96598
|
let edges = [];
|
|
96508
96599
|
try {
|
|
@@ -96560,12 +96651,14 @@ async function investigate(input, deps) {
|
|
|
96560
96651
|
);
|
|
96561
96652
|
flowEvIds.push(ev.id);
|
|
96562
96653
|
}
|
|
96563
|
-
|
|
96564
|
-
|
|
96565
|
-
|
|
96566
|
-
|
|
96567
|
-
|
|
96568
|
-
|
|
96654
|
+
if (impact) {
|
|
96655
|
+
impactEv = mkEv(
|
|
96656
|
+
"impact",
|
|
96657
|
+
`Impact of ${top.name}: ${impact.affected} affected symbol(s)`,
|
|
96658
|
+
{ affected: impact.affected },
|
|
96659
|
+
{ symbolId: top.id, file: top.filePath }
|
|
96660
|
+
);
|
|
96661
|
+
}
|
|
96569
96662
|
for (const edge of queueHits) {
|
|
96570
96663
|
const producer = edge.producerSymbol ?? "unknown-producer";
|
|
96571
96664
|
const worker = edge.workerSymbol ?? "unknown-worker";
|
|
@@ -97685,6 +97778,19 @@ async function investigate(input, deps) {
|
|
|
97685
97778
|
});
|
|
97686
97779
|
}
|
|
97687
97780
|
}
|
|
97781
|
+
if (top && seedEv && ctx && !sourceImpactHint) {
|
|
97782
|
+
const dataFlow = detectDataFlowCause(ctx);
|
|
97783
|
+
if (dataFlow) {
|
|
97784
|
+
causeInputs.push({
|
|
97785
|
+
id: "cause:seed-data-flow",
|
|
97786
|
+
title: dataFlow.title.slice(0, 220),
|
|
97787
|
+
category: "data-flow",
|
|
97788
|
+
sourceEvidenceIds: [seedEv.id],
|
|
97789
|
+
baseScore: dataFlow.baseScore,
|
|
97790
|
+
metadata: { dataFlowPattern: dataFlow.pattern, matched: dataFlow.matched, blastRadius }
|
|
97791
|
+
});
|
|
97792
|
+
}
|
|
97793
|
+
}
|
|
97688
97794
|
if (analysis !== null && analysis.signatures.length > 0 && queueHits.length > 0 && directLogEvIds.length > 0) {
|
|
97689
97795
|
const firstQueue = queueHits[0];
|
|
97690
97796
|
const queueLabel = firstQueue !== void 0 ? `"${firstQueue.queueName}" (${firstQueue.producerSymbol ?? "unknown"} -> ${firstQueue.workerSymbol ?? "unknown"})` : "the queue path";
|