@hiveai/core 0.12.4 → 0.12.9
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/README.md +6 -0
- package/dist/index.d.ts +181 -1
- package/dist/index.js +338 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/Doucs91/hAIve">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/Doucs91/hAIve/main/packages/vscode/media/logo.svg" alt="hAIve logo" width="96" />
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
1
7
|
# @hiveai/core
|
|
2
8
|
|
|
3
9
|
> Internal library — policy, memory, anchor, and enforcement primitives for hAIve.
|
package/dist/index.d.ts
CHANGED
|
@@ -634,6 +634,30 @@ declare function aggregateSensors(cases: SensorCaseResult[]): SensorAggregate;
|
|
|
634
634
|
/** Combine retrieval + sensor aggregates into a single 0..100 quality score. */
|
|
635
635
|
declare function overallScore(retrieval: RetrievalAggregate | null, sensors: SensorAggregate | null): number;
|
|
636
636
|
declare function buildReport(retrieval: RetrievalAggregate | null, sensors: SensorAggregate | null): EvalReport;
|
|
637
|
+
/**
|
|
638
|
+
* Baseline / compare — makes the "hAIve improves retrieval by N%" claim reproducible.
|
|
639
|
+
*
|
|
640
|
+
* `haive eval --baseline` snapshots a report; `--compare` re-runs and diffs against it,
|
|
641
|
+
* so a ranking/sensor regression is a number, not a vibe. Pure math here; the CLI does I/O.
|
|
642
|
+
*/
|
|
643
|
+
interface MetricDelta {
|
|
644
|
+
baseline: number;
|
|
645
|
+
current: number;
|
|
646
|
+
/** current − baseline (positive = improvement for all these metrics). */
|
|
647
|
+
delta: number;
|
|
648
|
+
}
|
|
649
|
+
interface EvalDelta {
|
|
650
|
+
score: MetricDelta;
|
|
651
|
+
mean_recall: MetricDelta | null;
|
|
652
|
+
mrr: MetricDelta | null;
|
|
653
|
+
catch_rate: MetricDelta | null;
|
|
654
|
+
/** True when the overall score dropped vs the baseline. */
|
|
655
|
+
regressed: boolean;
|
|
656
|
+
/** True when the overall score rose vs the baseline. */
|
|
657
|
+
improved: boolean;
|
|
658
|
+
}
|
|
659
|
+
/** Diff a current report against a baseline. Pure. */
|
|
660
|
+
declare function compareEvalReports(baseline: EvalReport, current: EvalReport): EvalDelta;
|
|
637
661
|
/** Extract a short task-like title from a memory body (first heading or first line). */
|
|
638
662
|
declare function titleFromBody(body: string): string;
|
|
639
663
|
interface SelfEvalOptions {
|
|
@@ -1466,4 +1490,160 @@ interface SensorSuggestionOptions {
|
|
|
1466
1490
|
*/
|
|
1467
1491
|
declare function suggestSensorFromMemory(body: string, anchorPaths: string[], options?: SensorSuggestionOptions): Sensor | null;
|
|
1468
1492
|
|
|
1469
|
-
|
|
1493
|
+
/**
|
|
1494
|
+
* Findings ingestion — the self-feeding half of the sensors story (feature B).
|
|
1495
|
+
*
|
|
1496
|
+
* Phase 1/2 turned a documented mistake into an executable `sensor`. But someone still has
|
|
1497
|
+
* to *document* the mistake. Findings ingestion closes the review↔memory loop: a real defect
|
|
1498
|
+
* reported by a scanner (SonarQube, or any SARIF-emitting tool like ESLint/Semgrep/CodeQL)
|
|
1499
|
+
* becomes an anchored `gotcha`/`convention` memory, pre-filled with a conservative autogen
|
|
1500
|
+
* sensor, so the *next* agent is steered away from it before it writes the same code.
|
|
1501
|
+
*
|
|
1502
|
+
* This module is pure: parsers + draft synthesis, no I/O. The CLI (`haive ingest`) and the
|
|
1503
|
+
* MCP tool (`ingest_findings`) read files / write memories around these functions.
|
|
1504
|
+
*
|
|
1505
|
+
* Safety: every draft is `status: proposed` and every suggested sensor is `severity: warn`
|
|
1506
|
+
* + `autogen: true`. Ingestion never auto-validates and never auto-blocks (safety rules +
|
|
1507
|
+
* `2026-05-07-attempt-strict-precommit-gate-on-haive`). A human promotes both.
|
|
1508
|
+
*/
|
|
1509
|
+
type FindingSeverity = "info" | "minor" | "major" | "critical" | "blocker";
|
|
1510
|
+
interface Finding {
|
|
1511
|
+
/** Source tool, e.g. "sonar", "eslint", "semgrep". */
|
|
1512
|
+
tool: string;
|
|
1513
|
+
/** Rule key, e.g. "typescript:S1234" or "no-unused-vars". */
|
|
1514
|
+
ruleId: string;
|
|
1515
|
+
/** Human-readable description of the problem. */
|
|
1516
|
+
message: string;
|
|
1517
|
+
severity: FindingSeverity;
|
|
1518
|
+
/** Project-relative file path. */
|
|
1519
|
+
path: string;
|
|
1520
|
+
/** 1-based line number, when known. */
|
|
1521
|
+
line?: number;
|
|
1522
|
+
/** Offending source snippet, when the report provides one. */
|
|
1523
|
+
snippet?: string;
|
|
1524
|
+
/**
|
|
1525
|
+
* Stable dedup key: `tool:ruleId:path`. Deliberately excludes the line so re-running a
|
|
1526
|
+
* scan after unrelated edits (which shift line numbers) does not re-propose the same memory.
|
|
1527
|
+
*/
|
|
1528
|
+
key: string;
|
|
1529
|
+
}
|
|
1530
|
+
interface MemoryDraft {
|
|
1531
|
+
key: string;
|
|
1532
|
+
/** `ingest:<key>` — used as the memory `topic` so re-ingestion upserts instead of duplicating. */
|
|
1533
|
+
topic: string;
|
|
1534
|
+
frontmatter: MemoryFrontmatter;
|
|
1535
|
+
body: string;
|
|
1536
|
+
finding: Finding;
|
|
1537
|
+
/** True when a conservative sensor could be derived from the finding. */
|
|
1538
|
+
has_sensor: boolean;
|
|
1539
|
+
}
|
|
1540
|
+
interface DraftOptions {
|
|
1541
|
+
/** Memory type for the draft. Default "gotcha". */
|
|
1542
|
+
type?: "gotcha" | "convention";
|
|
1543
|
+
/** Scope for the draft. Default "team". */
|
|
1544
|
+
scope?: "personal" | "team" | "module";
|
|
1545
|
+
module?: string;
|
|
1546
|
+
author?: string;
|
|
1547
|
+
}
|
|
1548
|
+
interface DraftsOptions extends DraftOptions {
|
|
1549
|
+
/** Cap on number of drafts produced (after in-batch dedup). */
|
|
1550
|
+
limit?: number;
|
|
1551
|
+
/** Only ingest findings at or above this severity. Default: none (all). */
|
|
1552
|
+
minSeverity?: FindingSeverity;
|
|
1553
|
+
}
|
|
1554
|
+
/** Normalize a tool-specific severity string to the shared scale. */
|
|
1555
|
+
declare function normalizeFindingSeverity(raw: string | undefined | null): FindingSeverity;
|
|
1556
|
+
/**
|
|
1557
|
+
* Parse SARIF 2.1.0 (`runs[].results[]`). Works for any SARIF emitter (ESLint, Semgrep,
|
|
1558
|
+
* CodeQL, etc.). The tool name comes from `runs[].tool.driver.name`.
|
|
1559
|
+
*/
|
|
1560
|
+
declare function parseSarif(input: string | unknown): Finding[];
|
|
1561
|
+
/**
|
|
1562
|
+
* Parse the SonarQube issues payload (`issues[]` from `/api/issues/search`). The file path
|
|
1563
|
+
* lives in `component` as `projectKey:relative/path`; we strip the project key.
|
|
1564
|
+
*/
|
|
1565
|
+
declare function parseSonar(input: string | unknown): Finding[];
|
|
1566
|
+
/** Dispatch to the right parser by declared format. */
|
|
1567
|
+
declare function parseFindings(format: "sarif" | "sonar", input: string | unknown): Finding[];
|
|
1568
|
+
/** Build the markdown body for a finding-derived memory. */
|
|
1569
|
+
declare function findingBody(finding: Finding): string;
|
|
1570
|
+
/** Convert one finding into a proposed memory draft (with a conservative sensor when derivable). */
|
|
1571
|
+
declare function findingToDraft(finding: Finding, options?: DraftOptions): MemoryDraft;
|
|
1572
|
+
/** Convert a batch of findings into drafts, deduped within the batch and capped/filtered. */
|
|
1573
|
+
declare function draftsFromFindings(findings: Finding[], options?: DraftsOptions): MemoryDraft[];
|
|
1574
|
+
/** Drop drafts whose topic already exists in the corpus (cross-run dedup). */
|
|
1575
|
+
declare function filterNewDrafts(drafts: MemoryDraft[], existingTopics: Iterable<string>): MemoryDraft[];
|
|
1576
|
+
|
|
1577
|
+
interface DashboardOptions {
|
|
1578
|
+
/** How many rows to include in each "top" list. Default 10. */
|
|
1579
|
+
top?: number;
|
|
1580
|
+
/** Dormancy window for impact scoring. Defaults to impact's own default. */
|
|
1581
|
+
dormantDays?: number;
|
|
1582
|
+
now?: Date;
|
|
1583
|
+
}
|
|
1584
|
+
interface ImpactRow {
|
|
1585
|
+
id: string;
|
|
1586
|
+
score: number;
|
|
1587
|
+
tier: ImpactScore["tier"];
|
|
1588
|
+
signals: string[];
|
|
1589
|
+
prune_candidate: boolean;
|
|
1590
|
+
}
|
|
1591
|
+
interface SensorRow {
|
|
1592
|
+
id: string;
|
|
1593
|
+
severity: "warn" | "block";
|
|
1594
|
+
last_fired: string;
|
|
1595
|
+
}
|
|
1596
|
+
interface DormantRow {
|
|
1597
|
+
id: string;
|
|
1598
|
+
last_read_at: string | null;
|
|
1599
|
+
age_days: number;
|
|
1600
|
+
}
|
|
1601
|
+
interface DashboardReport {
|
|
1602
|
+
generated_at: string;
|
|
1603
|
+
inventory: {
|
|
1604
|
+
/** Policy corpus size (excludes session_recap). */
|
|
1605
|
+
total: number;
|
|
1606
|
+
session_recaps: number;
|
|
1607
|
+
active: number;
|
|
1608
|
+
retired: number;
|
|
1609
|
+
by_scope: Record<string, number>;
|
|
1610
|
+
by_type: Record<string, number>;
|
|
1611
|
+
by_status: Record<string, number>;
|
|
1612
|
+
};
|
|
1613
|
+
impact: ImpactSummary & {
|
|
1614
|
+
top: ImpactRow[];
|
|
1615
|
+
};
|
|
1616
|
+
sensors: {
|
|
1617
|
+
total: number;
|
|
1618
|
+
warn: number;
|
|
1619
|
+
block: number;
|
|
1620
|
+
autogen: number;
|
|
1621
|
+
fired: number;
|
|
1622
|
+
recently_fired: SensorRow[];
|
|
1623
|
+
};
|
|
1624
|
+
health: {
|
|
1625
|
+
stale: number;
|
|
1626
|
+
retired: number;
|
|
1627
|
+
/** Validated decision/gotcha/architecture memories with no anchor paths or symbols. */
|
|
1628
|
+
anchorless: number;
|
|
1629
|
+
/** Memories awaiting review (draft/proposed). */
|
|
1630
|
+
pending: number;
|
|
1631
|
+
prune_candidates: number;
|
|
1632
|
+
};
|
|
1633
|
+
decay: {
|
|
1634
|
+
decay_days: number;
|
|
1635
|
+
decaying: number;
|
|
1636
|
+
top_dormant: DormantRow[];
|
|
1637
|
+
};
|
|
1638
|
+
corpus: {
|
|
1639
|
+
/** Number of memory files (policy corpus, excludes session_recap). */
|
|
1640
|
+
memory_files: number;
|
|
1641
|
+
body_chars: number;
|
|
1642
|
+
/** Rough token estimate (~chars/4) — how heavy the corpus is to inject. */
|
|
1643
|
+
est_tokens: number;
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
/** Build the full observability rollup from the loaded corpus + usage index. Pure. */
|
|
1647
|
+
declare function buildDashboard(memories: LoadedMemory[], usage: UsageIndex, options?: DashboardOptions): DashboardReport;
|
|
1648
|
+
|
|
1649
|
+
export { AUTOPILOT_DEFAULTS, type Activation, type ActivationContext, ActivationSchema, type Anchor, AnchorSchema, type AntiPatternGate, type AutoPromoteRule, BRIEFING_MARKER_TTL_MS, BRIEFING_PRESET_DEFAULTS, type BreakingChange, type BriefingBudgetNumbers, type BriefingBudgetPreset, type BriefingMarker, type BudgetPart, type BudgetSlice, type BuildCodeMapOptions, CHARS_PER_TOKEN, CODE_MAP_FILE, CODE_STOPWORDS, CONFIG_FILE, type CodeExport, type CodeExportKind, type CodeFileEntry, type CodeMap, type CodeMapQueryOptions, type CollectTimelineOpts, type ConfidenceLevel, type ConfidenceThresholds, type ConflictCandidatePair, type ConflictCandidatesOpts, type ContractDiffResult, type ContractFile, type ContractSnapshot, CrossRepoProvenanceSchema, type CrossRepoReport, type CrossRepoSource, DECAY_DAYS, DEFAULT_AUTO_PROMOTE_RULE, DEFAULT_CONFIDENCE_THRESHOLDS, DEFAULT_CONFIG, DEFAULT_DORMANT_DAYS, type DashboardOptions, type DashboardReport, type DepChange, type DepTrackResult, type DependencySnapshot, type DocFrequency, type DormantRow, type DraftOptions, type DraftsOptions, type EvalDelta, type EvalReport, type EvalSpec, type Finding, type FindingSeverity, GUESSABLE_THRESHOLD, HAIVE_DIR, type HaiveConfig, type HaivePaths, type ImpactOptions, type ImpactRow, type ImpactScore, type ImpactSummary, type ImpactTier, type LexicalRankResult, type LoadedMemory, MEMORIES_DIR, MIN_WORD_LEN, type Memory, type MemoryDraft, type MemoryFrontmatter, MemoryFrontmatterSchema, type MemoryScope, MemoryScopeSchema, type MemoryStatus, MemoryStatusSchema, type MemoryType, MemoryTypeSchema, type MemoryUsage, type MetricDelta, PROJECT_CONTEXT_FILE, RUNTIME_JOURNAL_FILENAME, type ResolveProjectInfo, type RetirementSignal, type RetrievalAggregate, type RetrievalCase, type RetrievalCaseResult, type RuntimeJournalEntry, SESSION_RECAP_TTL_MS, STACK_PACK_TAG, type SelfEvalOptions, type Sensor, type SensorAggregate, type SensorCase, type SensorCaseResult, type SensorHit, type SensorRow, SensorSchema, type SensorSuggestionOptions, type SensorTarget, type SkillActivation, type TimelineEntry, type TopicStatusPair, type TruncateOptions, type TruncateResult, USAGE_FILE, USAGE_LOG_DIR, USAGE_LOG_FILE, type UsageAggregate, type UsageEvent, type UsageIndex, type VerifyOptions, type VerifyResult, addedLinesFromDiff, aggregateRetrieval, aggregateSensors, aggregateUsage, allocateBudget, antiPatternGateParams, appendRuntimeJournalEntry, appendUsageEvent, briefingMarkerPath, briefingMarkersDir, buildCodeMap, buildDashboard, buildDocFrequency, buildFrontmatter, buildReport, bumpRead, codeMapPath, collectTimelineEntries, compareEvalReports, compareImpact, compileRegexSensor, computeImpact, configPath, contractLockPath, deriveConfidence, diffContract, diffHasDistinctiveOverlap, distinctiveCap, draftsFromFindings, emptyUsage, emptyUsageIndex, enforcementDir, estimateTokens, evaluateSkillActivation, extractActionsBriefBody, extractSnippet, filterNewDrafts, findLexicalConflictPairs, findProjectRoot, findTopicStatusConflictPairs, findingBody, findingToDraft, firstMemoryOneLine, getUsage, globToRegExp, hasRecentBriefingMarker, inferModulesFromPaths, isAutoPromoteEligible, isDecaying, isDistinctiveToken, isFreshIsoDate, isGlobPath, isLikelyGuessable, isRetiredMemory, isSkill, isSkillSuppressed, isStackPackSeed, listMarkdownFilesRecursive, literalMatchesAllTokens, literalMatchesAnyToken, loadCodeMap, loadConfig, loadConfigSync, loadMemoriesFromDir, loadMemory, loadUsageIndex, memoryFilePath, memoryMatchesAnchorPaths, newMemoryId, normalizeFindingSeverity, normalizeSessionId, overallScore, parseFindings, parseMemory, parseSarif, parseSince, parseSonar, pathsOverlap, pickSnippetNeedle, pullCrossRepoSources, queryCodeMap, rankMemoriesLexical, readRecentBriefingMarker, readRuntimeJournalTail, readUsageEvents, recordApplied, recordRejection, relPathFrom, resolveBriefingBudget, resolveHaivePaths, resolveManifestFiles, resolveProjectInfo, retirementSignal, runRegexSensor, runSensors, runtimeJournalPath, saveCodeMap, saveConfig, saveUsageIndex, scoreRetrievalCase, scoreSensorCase, sensorAppliesToPath, sensorTargetsFromDiff, serializeMemory, snapshotContract, specificityScore, stripPrivate, suggestSensorFromMemory, suggestTopicKey, summarizeImpact, synthesizeSelfEvalCases, titleFromBody, tokenizeQuery, tokenizeWords, trackDependencies, trackReads, truncateToTokens, usageLogPath, usageLogSize, usagePath, verifyAnchor, watchContracts, writeBriefingMarker };
|
package/dist/index.js
CHANGED
|
@@ -850,6 +850,22 @@ function overallScore(retrieval, sensors) {
|
|
|
850
850
|
function buildReport(retrieval, sensors) {
|
|
851
851
|
return { retrieval, sensors, score: overallScore(retrieval, sensors) };
|
|
852
852
|
}
|
|
853
|
+
function metricDelta(baseline, current) {
|
|
854
|
+
return { baseline: round32(baseline), current: round32(current), delta: round32(current - baseline) };
|
|
855
|
+
}
|
|
856
|
+
function compareEvalReports(baseline, current) {
|
|
857
|
+
const recall = baseline.retrieval && current.retrieval ? metricDelta(baseline.retrieval.mean_recall, current.retrieval.mean_recall) : null;
|
|
858
|
+
const mrr = baseline.retrieval && current.retrieval ? metricDelta(baseline.retrieval.mrr, current.retrieval.mrr) : null;
|
|
859
|
+
const catchRate = baseline.sensors && current.sensors ? metricDelta(baseline.sensors.catch_rate, current.sensors.catch_rate) : null;
|
|
860
|
+
return {
|
|
861
|
+
score: metricDelta(baseline.score, current.score),
|
|
862
|
+
mean_recall: recall,
|
|
863
|
+
mrr,
|
|
864
|
+
catch_rate: catchRate,
|
|
865
|
+
regressed: current.score < baseline.score,
|
|
866
|
+
improved: current.score > baseline.score
|
|
867
|
+
};
|
|
868
|
+
}
|
|
853
869
|
function titleFromBody(body) {
|
|
854
870
|
const lines = body.split("\n");
|
|
855
871
|
for (const line of lines) {
|
|
@@ -3056,6 +3072,318 @@ function sensorMessageFromBody(body, token) {
|
|
|
3056
3072
|
function escapeRegExp(value) {
|
|
3057
3073
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3058
3074
|
}
|
|
3075
|
+
|
|
3076
|
+
// src/findings.ts
|
|
3077
|
+
var SEVERITY_RANK = {
|
|
3078
|
+
info: 0,
|
|
3079
|
+
minor: 1,
|
|
3080
|
+
major: 2,
|
|
3081
|
+
critical: 3,
|
|
3082
|
+
blocker: 4
|
|
3083
|
+
};
|
|
3084
|
+
function normalizeFindingSeverity(raw) {
|
|
3085
|
+
const v = (raw ?? "").toString().trim().toLowerCase();
|
|
3086
|
+
switch (v) {
|
|
3087
|
+
case "blocker":
|
|
3088
|
+
return "blocker";
|
|
3089
|
+
case "critical":
|
|
3090
|
+
case "error":
|
|
3091
|
+
case "fatal":
|
|
3092
|
+
return "critical";
|
|
3093
|
+
case "major":
|
|
3094
|
+
case "warning":
|
|
3095
|
+
case "warn":
|
|
3096
|
+
return "major";
|
|
3097
|
+
case "minor":
|
|
3098
|
+
case "note":
|
|
3099
|
+
case "info":
|
|
3100
|
+
case "information":
|
|
3101
|
+
case "informational":
|
|
3102
|
+
return v === "minor" ? "minor" : "info";
|
|
3103
|
+
default:
|
|
3104
|
+
return "info";
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
function findingKey(tool, ruleId, path15) {
|
|
3108
|
+
return `${tool}:${ruleId}:${path15}`;
|
|
3109
|
+
}
|
|
3110
|
+
function coerceJson(input) {
|
|
3111
|
+
if (typeof input === "string") {
|
|
3112
|
+
try {
|
|
3113
|
+
return JSON.parse(input);
|
|
3114
|
+
} catch (err) {
|
|
3115
|
+
throw new Error(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
return input;
|
|
3119
|
+
}
|
|
3120
|
+
function asArray(value) {
|
|
3121
|
+
return Array.isArray(value) ? value : [];
|
|
3122
|
+
}
|
|
3123
|
+
function asRecord(value) {
|
|
3124
|
+
return value && typeof value === "object" ? value : {};
|
|
3125
|
+
}
|
|
3126
|
+
function parseSarif(input) {
|
|
3127
|
+
const doc = asRecord(coerceJson(input));
|
|
3128
|
+
const findings = [];
|
|
3129
|
+
for (const runRaw of asArray(doc.runs)) {
|
|
3130
|
+
const run = asRecord(runRaw);
|
|
3131
|
+
const driver = asRecord(asRecord(run.tool).driver);
|
|
3132
|
+
const tool = (typeof driver.name === "string" ? driver.name : "sarif").toLowerCase();
|
|
3133
|
+
for (const resultRaw of asArray(run.results)) {
|
|
3134
|
+
const result = asRecord(resultRaw);
|
|
3135
|
+
const ruleId = typeof result.ruleId === "string" && result.ruleId || (typeof asRecord(result.rule).id === "string" ? asRecord(result.rule).id : "") || "unknown-rule";
|
|
3136
|
+
const message = (typeof asRecord(result.message).text === "string" ? asRecord(result.message).text : "") || ruleId;
|
|
3137
|
+
const severity = normalizeFindingSeverity(typeof result.level === "string" ? result.level : "warning");
|
|
3138
|
+
const location = asRecord(asArray(result.locations)[0]);
|
|
3139
|
+
const physical = asRecord(location.physicalLocation);
|
|
3140
|
+
const artifact = asRecord(physical.artifactLocation);
|
|
3141
|
+
const region = asRecord(physical.region);
|
|
3142
|
+
const path15 = typeof artifact.uri === "string" ? normalizeUri(artifact.uri) : "";
|
|
3143
|
+
if (!path15) continue;
|
|
3144
|
+
const line = typeof region.startLine === "number" ? region.startLine : void 0;
|
|
3145
|
+
const snippet = typeof asRecord(region.snippet).text === "string" ? asRecord(region.snippet).text.trim() : void 0;
|
|
3146
|
+
findings.push({
|
|
3147
|
+
tool,
|
|
3148
|
+
ruleId,
|
|
3149
|
+
message: message.trim(),
|
|
3150
|
+
severity,
|
|
3151
|
+
path: path15,
|
|
3152
|
+
...line !== void 0 ? { line } : {},
|
|
3153
|
+
...snippet ? { snippet } : {},
|
|
3154
|
+
key: findingKey(tool, ruleId, path15)
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
return findings;
|
|
3159
|
+
}
|
|
3160
|
+
function parseSonar(input) {
|
|
3161
|
+
const doc = asRecord(coerceJson(input));
|
|
3162
|
+
const findings = [];
|
|
3163
|
+
for (const issueRaw of asArray(doc.issues)) {
|
|
3164
|
+
const issue = asRecord(issueRaw);
|
|
3165
|
+
const ruleId = typeof issue.rule === "string" ? issue.rule : "unknown-rule";
|
|
3166
|
+
const message = typeof issue.message === "string" ? issue.message.trim() : ruleId;
|
|
3167
|
+
const impacts = asArray(issue.impacts);
|
|
3168
|
+
const impactSeverity = impacts.length > 0 && typeof asRecord(impacts[0]).severity === "string" ? asRecord(impacts[0]).severity : void 0;
|
|
3169
|
+
const severity = normalizeFindingSeverity(
|
|
3170
|
+
(typeof issue.severity === "string" ? issue.severity : void 0) ?? impactSeverity
|
|
3171
|
+
);
|
|
3172
|
+
const component = typeof issue.component === "string" ? issue.component : "";
|
|
3173
|
+
const path15 = componentToPath(component);
|
|
3174
|
+
if (!path15) continue;
|
|
3175
|
+
const line = typeof issue.line === "number" ? issue.line : void 0;
|
|
3176
|
+
findings.push({
|
|
3177
|
+
tool: "sonar",
|
|
3178
|
+
ruleId,
|
|
3179
|
+
message,
|
|
3180
|
+
severity,
|
|
3181
|
+
path: path15,
|
|
3182
|
+
...line !== void 0 ? { line } : {},
|
|
3183
|
+
key: findingKey("sonar", ruleId, path15)
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
3186
|
+
return findings;
|
|
3187
|
+
}
|
|
3188
|
+
function parseFindings(format, input) {
|
|
3189
|
+
return format === "sonar" ? parseSonar(input) : parseSarif(input);
|
|
3190
|
+
}
|
|
3191
|
+
function normalizeUri(uri) {
|
|
3192
|
+
return uri.replace(/^file:\/\//, "").replace(/^\.\//, "");
|
|
3193
|
+
}
|
|
3194
|
+
function componentToPath(component) {
|
|
3195
|
+
const idx = component.indexOf(":");
|
|
3196
|
+
return idx === -1 ? component : component.slice(idx + 1);
|
|
3197
|
+
}
|
|
3198
|
+
function sanitize(value) {
|
|
3199
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
3200
|
+
}
|
|
3201
|
+
function basename(p) {
|
|
3202
|
+
const parts = p.split("/");
|
|
3203
|
+
return parts[parts.length - 1] || p;
|
|
3204
|
+
}
|
|
3205
|
+
function findingBody(finding) {
|
|
3206
|
+
const lines = [
|
|
3207
|
+
`# ${finding.ruleId} in ${finding.path}`,
|
|
3208
|
+
"",
|
|
3209
|
+
`**Source:** ${finding.tool} finding (severity: ${finding.severity})`,
|
|
3210
|
+
"",
|
|
3211
|
+
finding.message,
|
|
3212
|
+
"",
|
|
3213
|
+
`**Location:** ${finding.path}${finding.line !== void 0 ? `:${finding.line}` : ""}`
|
|
3214
|
+
];
|
|
3215
|
+
if (finding.snippet) {
|
|
3216
|
+
lines.push("", "**Offending code:**", "```", finding.snippet, "```");
|
|
3217
|
+
}
|
|
3218
|
+
lines.push("", `**Instead, use:** Resolve the ${finding.tool} finding \u2014 ${finding.message}`);
|
|
3219
|
+
return lines.join("\n") + "\n";
|
|
3220
|
+
}
|
|
3221
|
+
function findingToDraft(finding, options = {}) {
|
|
3222
|
+
const type = options.type ?? "gotcha";
|
|
3223
|
+
const scope = options.scope ?? "team";
|
|
3224
|
+
const topic = `ingest:${finding.key}`;
|
|
3225
|
+
const slug = `${sanitize(finding.tool)}-${sanitize(finding.ruleId)}-${sanitize(basename(finding.path))}`;
|
|
3226
|
+
const body = findingBody(finding);
|
|
3227
|
+
const frontmatter = buildFrontmatter({
|
|
3228
|
+
type,
|
|
3229
|
+
slug,
|
|
3230
|
+
scope,
|
|
3231
|
+
module: options.module,
|
|
3232
|
+
author: options.author,
|
|
3233
|
+
paths: [finding.path],
|
|
3234
|
+
tags: ["ingested", finding.tool, finding.severity],
|
|
3235
|
+
topic,
|
|
3236
|
+
status: "proposed"
|
|
3237
|
+
});
|
|
3238
|
+
const sensor = suggestSensorFromMemory(body, [finding.path]);
|
|
3239
|
+
if (sensor) frontmatter.sensor = sensor;
|
|
3240
|
+
return { key: finding.key, topic, frontmatter, body, finding, has_sensor: Boolean(sensor) };
|
|
3241
|
+
}
|
|
3242
|
+
function draftsFromFindings(findings, options = {}) {
|
|
3243
|
+
const minRank = options.minSeverity ? SEVERITY_RANK[options.minSeverity] : -1;
|
|
3244
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3245
|
+
const drafts = [];
|
|
3246
|
+
for (const finding of findings) {
|
|
3247
|
+
if (SEVERITY_RANK[finding.severity] < minRank) continue;
|
|
3248
|
+
if (seen.has(finding.key)) continue;
|
|
3249
|
+
seen.add(finding.key);
|
|
3250
|
+
drafts.push(findingToDraft(finding, options));
|
|
3251
|
+
if (options.limit !== void 0 && drafts.length >= options.limit) break;
|
|
3252
|
+
}
|
|
3253
|
+
return drafts;
|
|
3254
|
+
}
|
|
3255
|
+
function filterNewDrafts(drafts, existingTopics) {
|
|
3256
|
+
const existing = new Set(existingTopics);
|
|
3257
|
+
return drafts.filter((d) => !existing.has(d.topic));
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
// src/dashboard.ts
|
|
3261
|
+
var MS_PER_DAY3 = 24 * 60 * 60 * 1e3;
|
|
3262
|
+
function isAnchorless(fm) {
|
|
3263
|
+
if (!["decision", "gotcha", "architecture"].includes(fm.type)) return false;
|
|
3264
|
+
if (fm.status !== "validated") return false;
|
|
3265
|
+
return fm.anchor.paths.length === 0 && fm.anchor.symbols.length === 0;
|
|
3266
|
+
}
|
|
3267
|
+
function inc(map, key) {
|
|
3268
|
+
map[key] = (map[key] ?? 0) + 1;
|
|
3269
|
+
}
|
|
3270
|
+
function buildDashboard(memories, usage, options = {}) {
|
|
3271
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
3272
|
+
const top = options.top ?? 10;
|
|
3273
|
+
const inventory = {
|
|
3274
|
+
total: 0,
|
|
3275
|
+
session_recaps: 0,
|
|
3276
|
+
active: 0,
|
|
3277
|
+
retired: 0,
|
|
3278
|
+
by_scope: {},
|
|
3279
|
+
by_type: {},
|
|
3280
|
+
by_status: {}
|
|
3281
|
+
};
|
|
3282
|
+
const impactScores = [];
|
|
3283
|
+
const impactRows = [];
|
|
3284
|
+
const sensorRows = [];
|
|
3285
|
+
let sensorTotal = 0;
|
|
3286
|
+
let sensorWarn = 0;
|
|
3287
|
+
let sensorBlock = 0;
|
|
3288
|
+
let sensorAutogen = 0;
|
|
3289
|
+
let sensorFired = 0;
|
|
3290
|
+
let stale = 0;
|
|
3291
|
+
let retired = 0;
|
|
3292
|
+
let anchorless = 0;
|
|
3293
|
+
let pending = 0;
|
|
3294
|
+
let decaying = 0;
|
|
3295
|
+
let bodyChars = 0;
|
|
3296
|
+
const dormantRows = [];
|
|
3297
|
+
for (const { memory } of memories) {
|
|
3298
|
+
const fm = memory.frontmatter;
|
|
3299
|
+
if (fm.type === "session_recap") {
|
|
3300
|
+
inventory.session_recaps += 1;
|
|
3301
|
+
continue;
|
|
3302
|
+
}
|
|
3303
|
+
inventory.total += 1;
|
|
3304
|
+
inc(inventory.by_scope, fm.scope);
|
|
3305
|
+
inc(inventory.by_type, fm.type);
|
|
3306
|
+
inc(inventory.by_status, fm.status);
|
|
3307
|
+
bodyChars += memory.body.length;
|
|
3308
|
+
const isRetired = isRetiredMemory(fm, memory.body, now);
|
|
3309
|
+
if (isRetired) {
|
|
3310
|
+
inventory.retired += 1;
|
|
3311
|
+
retired += 1;
|
|
3312
|
+
} else {
|
|
3313
|
+
inventory.active += 1;
|
|
3314
|
+
}
|
|
3315
|
+
if (fm.status === "stale") stale += 1;
|
|
3316
|
+
if (isAnchorless(fm)) anchorless += 1;
|
|
3317
|
+
if (fm.status === "draft" || fm.status === "proposed") pending += 1;
|
|
3318
|
+
if (fm.sensor) {
|
|
3319
|
+
sensorTotal += 1;
|
|
3320
|
+
if (fm.sensor.severity === "block") sensorBlock += 1;
|
|
3321
|
+
else sensorWarn += 1;
|
|
3322
|
+
if (fm.sensor.autogen) sensorAutogen += 1;
|
|
3323
|
+
if (fm.sensor.last_fired) {
|
|
3324
|
+
sensorFired += 1;
|
|
3325
|
+
sensorRows.push({ id: fm.id, severity: fm.sensor.severity, last_fired: fm.sensor.last_fired });
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
const memUsage = getUsage(usage, fm.id);
|
|
3329
|
+
const impact = computeImpact(fm, memUsage, {
|
|
3330
|
+
now,
|
|
3331
|
+
...options.dormantDays !== void 0 ? { dormantDays: options.dormantDays } : {}
|
|
3332
|
+
});
|
|
3333
|
+
impactScores.push(impact);
|
|
3334
|
+
impactRows.push({
|
|
3335
|
+
id: fm.id,
|
|
3336
|
+
score: impact.score,
|
|
3337
|
+
tier: impact.tier,
|
|
3338
|
+
signals: impact.signals,
|
|
3339
|
+
prune_candidate: impact.pruneCandidate
|
|
3340
|
+
});
|
|
3341
|
+
if (isDecaying(memUsage, fm.created_at)) decaying += 1;
|
|
3342
|
+
if (impact.tier === "dormant") {
|
|
3343
|
+
const anchor = memUsage.last_read_at ?? fm.created_at;
|
|
3344
|
+
const ageDays = Math.floor((now.getTime() - new Date(anchor).getTime()) / MS_PER_DAY3);
|
|
3345
|
+
dormantRows.push({ id: fm.id, last_read_at: memUsage.last_read_at, age_days: ageDays });
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
impactRows.sort(
|
|
3349
|
+
(a, b) => compareImpact(
|
|
3350
|
+
{ score: a.score, tier: a.tier, signals: a.signals, pruneCandidate: a.prune_candidate },
|
|
3351
|
+
{ score: b.score, tier: b.tier, signals: b.signals, pruneCandidate: b.prune_candidate }
|
|
3352
|
+
)
|
|
3353
|
+
);
|
|
3354
|
+
sensorRows.sort((a, b) => b.last_fired.localeCompare(a.last_fired));
|
|
3355
|
+
dormantRows.sort((a, b) => b.age_days - a.age_days);
|
|
3356
|
+
return {
|
|
3357
|
+
generated_at: now.toISOString(),
|
|
3358
|
+
inventory,
|
|
3359
|
+
impact: { ...summarizeImpact(impactScores), top: impactRows.slice(0, top) },
|
|
3360
|
+
sensors: {
|
|
3361
|
+
total: sensorTotal,
|
|
3362
|
+
warn: sensorWarn,
|
|
3363
|
+
block: sensorBlock,
|
|
3364
|
+
autogen: sensorAutogen,
|
|
3365
|
+
fired: sensorFired,
|
|
3366
|
+
recently_fired: sensorRows.slice(0, top)
|
|
3367
|
+
},
|
|
3368
|
+
health: {
|
|
3369
|
+
stale,
|
|
3370
|
+
retired,
|
|
3371
|
+
anchorless,
|
|
3372
|
+
pending,
|
|
3373
|
+
prune_candidates: impactScores.filter((s) => s.pruneCandidate).length
|
|
3374
|
+
},
|
|
3375
|
+
decay: {
|
|
3376
|
+
decay_days: DECAY_DAYS,
|
|
3377
|
+
decaying,
|
|
3378
|
+
top_dormant: dormantRows.slice(0, top)
|
|
3379
|
+
},
|
|
3380
|
+
corpus: {
|
|
3381
|
+
memory_files: inventory.total,
|
|
3382
|
+
body_chars: bodyChars,
|
|
3383
|
+
est_tokens: Math.round(bodyChars / 4)
|
|
3384
|
+
}
|
|
3385
|
+
};
|
|
3386
|
+
}
|
|
3059
3387
|
export {
|
|
3060
3388
|
AUTOPILOT_DEFAULTS,
|
|
3061
3389
|
ActivationSchema,
|
|
@@ -3099,12 +3427,14 @@ export {
|
|
|
3099
3427
|
briefingMarkerPath,
|
|
3100
3428
|
briefingMarkersDir,
|
|
3101
3429
|
buildCodeMap,
|
|
3430
|
+
buildDashboard,
|
|
3102
3431
|
buildDocFrequency,
|
|
3103
3432
|
buildFrontmatter,
|
|
3104
3433
|
buildReport,
|
|
3105
3434
|
bumpRead,
|
|
3106
3435
|
codeMapPath,
|
|
3107
3436
|
collectTimelineEntries,
|
|
3437
|
+
compareEvalReports,
|
|
3108
3438
|
compareImpact,
|
|
3109
3439
|
compileRegexSensor,
|
|
3110
3440
|
computeImpact,
|
|
@@ -3114,6 +3444,7 @@ export {
|
|
|
3114
3444
|
diffContract,
|
|
3115
3445
|
diffHasDistinctiveOverlap,
|
|
3116
3446
|
distinctiveCap,
|
|
3447
|
+
draftsFromFindings,
|
|
3117
3448
|
emptyUsage,
|
|
3118
3449
|
emptyUsageIndex,
|
|
3119
3450
|
enforcementDir,
|
|
@@ -3121,9 +3452,12 @@ export {
|
|
|
3121
3452
|
evaluateSkillActivation,
|
|
3122
3453
|
extractActionsBriefBody,
|
|
3123
3454
|
extractSnippet,
|
|
3455
|
+
filterNewDrafts,
|
|
3124
3456
|
findLexicalConflictPairs,
|
|
3125
3457
|
findProjectRoot,
|
|
3126
3458
|
findTopicStatusConflictPairs,
|
|
3459
|
+
findingBody,
|
|
3460
|
+
findingToDraft,
|
|
3127
3461
|
firstMemoryOneLine,
|
|
3128
3462
|
getUsage,
|
|
3129
3463
|
globToRegExp,
|
|
@@ -3151,10 +3485,14 @@ export {
|
|
|
3151
3485
|
memoryFilePath,
|
|
3152
3486
|
memoryMatchesAnchorPaths,
|
|
3153
3487
|
newMemoryId,
|
|
3488
|
+
normalizeFindingSeverity,
|
|
3154
3489
|
normalizeSessionId,
|
|
3155
3490
|
overallScore,
|
|
3491
|
+
parseFindings,
|
|
3156
3492
|
parseMemory,
|
|
3493
|
+
parseSarif,
|
|
3157
3494
|
parseSince,
|
|
3495
|
+
parseSonar,
|
|
3158
3496
|
pathsOverlap,
|
|
3159
3497
|
pickSnippetNeedle,
|
|
3160
3498
|
pullCrossRepoSources,
|