@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 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
- 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 DepChange, type DepTrackResult, type DependencySnapshot, type DocFrequency, type EvalReport, type EvalSpec, GUESSABLE_THRESHOLD, HAIVE_DIR, type HaiveConfig, type HaivePaths, type ImpactOptions, type ImpactScore, type ImpactSummary, type ImpactTier, type LexicalRankResult, type LoadedMemory, MEMORIES_DIR, MIN_WORD_LEN, type Memory, type MemoryFrontmatter, MemoryFrontmatterSchema, type MemoryScope, MemoryScopeSchema, type MemoryStatus, MemoryStatusSchema, type MemoryType, MemoryTypeSchema, type MemoryUsage, 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, 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, buildDocFrequency, buildFrontmatter, buildReport, bumpRead, codeMapPath, collectTimelineEntries, compareImpact, compileRegexSensor, computeImpact, configPath, contractLockPath, deriveConfidence, diffContract, diffHasDistinctiveOverlap, distinctiveCap, emptyUsage, emptyUsageIndex, enforcementDir, estimateTokens, evaluateSkillActivation, extractActionsBriefBody, extractSnippet, findLexicalConflictPairs, findProjectRoot, findTopicStatusConflictPairs, 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, normalizeSessionId, overallScore, parseMemory, parseSince, 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 };
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,