@wutangbanger/horus-insight-store 1.0.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/AgentInsightStore.d.ts +25 -0
- package/dist/AgentInsightStore.d.ts.map +1 -0
- package/dist/AgentInsightStore.js +60 -0
- package/dist/AgentInsightStore.js.map +1 -0
- package/dist/CoverageStore.d.ts +23 -0
- package/dist/CoverageStore.d.ts.map +1 -0
- package/dist/CoverageStore.js +57 -0
- package/dist/CoverageStore.js.map +1 -0
- package/dist/EventContractAnalyzer.d.ts +38 -0
- package/dist/EventContractAnalyzer.d.ts.map +1 -0
- package/dist/EventContractAnalyzer.js +111 -0
- package/dist/EventContractAnalyzer.js.map +1 -0
- package/dist/FlakinesAnalyzer.d.ts +21 -0
- package/dist/FlakinesAnalyzer.d.ts.map +1 -0
- package/dist/FlakinesAnalyzer.js +54 -0
- package/dist/FlakinesAnalyzer.js.map +1 -0
- package/dist/HorusVitestReporter.d.ts +21 -0
- package/dist/HorusVitestReporter.d.ts.map +1 -0
- package/dist/HorusVitestReporter.js +54 -0
- package/dist/HorusVitestReporter.js.map +1 -0
- package/dist/TestRunStore.d.ts +20 -0
- package/dist/TestRunStore.d.ts.map +1 -0
- package/dist/TestRunStore.js +52 -0
- package/dist/TestRunStore.js.map +1 -0
- package/dist/glob.d.ts +6 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +44 -0
- package/dist/glob.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest.d.ts +15 -0
- package/dist/ingest.d.ts.map +1 -0
- package/dist/ingest.js +51 -0
- package/dist/ingest.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentInsightStore
|
|
3
|
+
*
|
|
4
|
+
* JSONL-backed persistence for AI agent insights. Each agent gets its own
|
|
5
|
+
* file: reports/agent-insights/<agentId>.jsonl — one JSON record per line.
|
|
6
|
+
*
|
|
7
|
+
* JSONL (newline-delimited JSON) is used because:
|
|
8
|
+
* - Appends are O(1) — no need to parse and rewrite the whole file
|
|
9
|
+
* - Each line is a valid JSON object, easy to stream or tail
|
|
10
|
+
* - Human-readable and grep-friendly
|
|
11
|
+
*
|
|
12
|
+
* Implements IAgentInsightStore from @horus/contracts.
|
|
13
|
+
*/
|
|
14
|
+
import { AgentInsight, IAgentInsightStore, HorusConfig } from '@horus/contracts';
|
|
15
|
+
export declare class AgentInsightStore implements IAgentInsightStore {
|
|
16
|
+
private readonly dir;
|
|
17
|
+
constructor(config: HorusConfig | string);
|
|
18
|
+
append(insight: AgentInsight): Promise<void>;
|
|
19
|
+
readAll(): Promise<AgentInsight[]>;
|
|
20
|
+
readSince(isoTimestamp: string): Promise<AgentInsight[]>;
|
|
21
|
+
readByAgent(agentId: string): Promise<AgentInsight[]>;
|
|
22
|
+
private filePathFor;
|
|
23
|
+
private readFile;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=AgentInsightStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AgentInsightStore.d.ts","sourceRoot":"","sources":["../src/AgentInsightStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEjF,qBAAa,iBAAkB,YAAW,kBAAkB;IAC1D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,MAAM,EAAE,WAAW,GAAG,MAAM;IAMlC,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5C,OAAO,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAalC,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAKxD,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAQ3D,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,QAAQ;CAOjB"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentInsightStore
|
|
3
|
+
*
|
|
4
|
+
* JSONL-backed persistence for AI agent insights. Each agent gets its own
|
|
5
|
+
* file: reports/agent-insights/<agentId>.jsonl — one JSON record per line.
|
|
6
|
+
*
|
|
7
|
+
* JSONL (newline-delimited JSON) is used because:
|
|
8
|
+
* - Appends are O(1) — no need to parse and rewrite the whole file
|
|
9
|
+
* - Each line is a valid JSON object, easy to stream or tail
|
|
10
|
+
* - Human-readable and grep-friendly
|
|
11
|
+
*
|
|
12
|
+
* Implements IAgentInsightStore from @horus/contracts.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
export class AgentInsightStore {
|
|
17
|
+
dir;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
const reportsDir = typeof config === 'string' ? config : config.reportsDir;
|
|
20
|
+
this.dir = path.resolve(reportsDir, 'agent-insights');
|
|
21
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
async append(insight) {
|
|
24
|
+
const filePath = this.filePathFor(insight.agentId);
|
|
25
|
+
const line = JSON.stringify(insight) + '\n';
|
|
26
|
+
fs.appendFileSync(filePath, line, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
async readAll() {
|
|
29
|
+
const files = fs.existsSync(this.dir)
|
|
30
|
+
? fs.readdirSync(this.dir).filter((f) => f.endsWith('.jsonl'))
|
|
31
|
+
: [];
|
|
32
|
+
const all = [];
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
all.push(...this.readFile(path.join(this.dir, file)));
|
|
35
|
+
}
|
|
36
|
+
return all.sort((a, b) => a.runAt.localeCompare(b.runAt));
|
|
37
|
+
}
|
|
38
|
+
async readSince(isoTimestamp) {
|
|
39
|
+
const all = await this.readAll();
|
|
40
|
+
return all.filter((insight) => insight.runAt >= isoTimestamp);
|
|
41
|
+
}
|
|
42
|
+
async readByAgent(agentId) {
|
|
43
|
+
const filePath = this.filePathFor(agentId);
|
|
44
|
+
if (!fs.existsSync(filePath))
|
|
45
|
+
return [];
|
|
46
|
+
return this.readFile(filePath);
|
|
47
|
+
}
|
|
48
|
+
// ── Private helpers ──────────────────────────────────────────────────────
|
|
49
|
+
filePathFor(agentId) {
|
|
50
|
+
return path.join(this.dir, `${agentId}.jsonl`);
|
|
51
|
+
}
|
|
52
|
+
readFile(filePath) {
|
|
53
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
54
|
+
return raw
|
|
55
|
+
.split('\n')
|
|
56
|
+
.filter((line) => line.trim().length > 0)
|
|
57
|
+
.map((line) => JSON.parse(line));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=AgentInsightStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AgentInsightStore.js","sourceRoot":"","sources":["../src/AgentInsightStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoverageStore
|
|
3
|
+
*
|
|
4
|
+
* Appends CoverageSnapshots to reports/coverage-history.jsonl after each
|
|
5
|
+
* test:coverage run. Provides helpers to compute deltas between runs.
|
|
6
|
+
*/
|
|
7
|
+
import { CoverageSnapshot, CoverageDelta, HorusConfig } from '@horus/contracts';
|
|
8
|
+
export declare class CoverageStore {
|
|
9
|
+
private readonly filePath;
|
|
10
|
+
private readonly thresholds;
|
|
11
|
+
constructor(config: HorusConfig | string);
|
|
12
|
+
append(snapshot: CoverageSnapshot): Promise<void>;
|
|
13
|
+
readAll(): Promise<CoverageSnapshot[]>;
|
|
14
|
+
/** Returns the delta between the two most recent snapshots, or null if fewer than 2 exist. */
|
|
15
|
+
latestDelta(): Promise<CoverageDelta | null>;
|
|
16
|
+
}
|
|
17
|
+
export declare function computeDelta(prev: CoverageSnapshot, curr: CoverageSnapshot, thresholds?: {
|
|
18
|
+
lines: number;
|
|
19
|
+
functions: number;
|
|
20
|
+
branches: number;
|
|
21
|
+
statements: number;
|
|
22
|
+
}): CoverageDelta;
|
|
23
|
+
//# sourceMappingURL=CoverageStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CoverageStore.d.ts","sourceRoot":"","sources":["../src/CoverageStore.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAIhF,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA4B;gBAE3C,MAAM,EAAE,WAAW,GAAG,MAAM;IAQlC,MAAM,CAAC,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjD,OAAO,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAU5C,8FAA8F;IACxF,WAAW,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;CAOnD;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,gBAAgB,EACtB,UAAU;;;;;CAAqB,GAC9B,aAAa,CAaf"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoverageStore
|
|
3
|
+
*
|
|
4
|
+
* Appends CoverageSnapshots to reports/coverage-history.jsonl after each
|
|
5
|
+
* test:coverage run. Provides helpers to compute deltas between runs.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
const DEFAULT_THRESHOLDS = { lines: 80, functions: 80, branches: 75, statements: 80 };
|
|
10
|
+
export class CoverageStore {
|
|
11
|
+
filePath;
|
|
12
|
+
thresholds;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
const reportsDir = typeof config === 'string' ? config : config.reportsDir;
|
|
15
|
+
this.thresholds = (typeof config !== 'string' ? config.coverage : undefined) ?? DEFAULT_THRESHOLDS;
|
|
16
|
+
const dir = path.resolve(reportsDir);
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
this.filePath = path.join(dir, 'coverage-history.jsonl');
|
|
19
|
+
}
|
|
20
|
+
async append(snapshot) {
|
|
21
|
+
fs.appendFileSync(this.filePath, JSON.stringify(snapshot) + '\n', 'utf8');
|
|
22
|
+
}
|
|
23
|
+
async readAll() {
|
|
24
|
+
if (!fs.existsSync(this.filePath))
|
|
25
|
+
return [];
|
|
26
|
+
const raw = fs.readFileSync(this.filePath, 'utf8');
|
|
27
|
+
return raw
|
|
28
|
+
.split('\n')
|
|
29
|
+
.filter((line) => line.trim().length > 0)
|
|
30
|
+
.map((line) => JSON.parse(line))
|
|
31
|
+
.sort((a, b) => a.capturedAt.localeCompare(b.capturedAt));
|
|
32
|
+
}
|
|
33
|
+
/** Returns the delta between the two most recent snapshots, or null if fewer than 2 exist. */
|
|
34
|
+
async latestDelta() {
|
|
35
|
+
const all = await this.readAll();
|
|
36
|
+
if (all.length < 2)
|
|
37
|
+
return null;
|
|
38
|
+
const prev = all[all.length - 2];
|
|
39
|
+
const curr = all[all.length - 1];
|
|
40
|
+
return computeDelta(prev, curr, this.thresholds);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function computeDelta(prev, curr, thresholds = DEFAULT_THRESHOLDS) {
|
|
44
|
+
const lines = round(curr.lines - prev.lines);
|
|
45
|
+
const functions = round(curr.functions - prev.functions);
|
|
46
|
+
const branches = round(curr.branches - prev.branches);
|
|
47
|
+
const statements = round(curr.statements - prev.statements);
|
|
48
|
+
const belowThreshold = curr.lines < thresholds.lines ||
|
|
49
|
+
curr.functions < thresholds.functions ||
|
|
50
|
+
curr.branches < thresholds.branches ||
|
|
51
|
+
curr.statements < thresholds.statements;
|
|
52
|
+
return { lines, functions, branches, statements, belowThreshold };
|
|
53
|
+
}
|
|
54
|
+
function round(n) {
|
|
55
|
+
return Math.round(n * 10) / 10;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=CoverageStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CoverageStore.js","sourceRoot":"","sources":["../src/CoverageStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventContractAnalyzer
|
|
3
|
+
*
|
|
4
|
+
* Static analyzer for event contract coverage gaps.
|
|
5
|
+
*
|
|
6
|
+
* For each topic declared in ORDER_EVENTS (or any `const X = { ... }` pattern
|
|
7
|
+
* matching event topic strings), it checks:
|
|
8
|
+
*
|
|
9
|
+
* 1. PUBLISH side — is there a test that asserts this topic was published?
|
|
10
|
+
* (looks for assertPublished / assertPublishedCount / mockEventBus calls)
|
|
11
|
+
* 2. SUBSCRIBE side — is there a test that exercises the handler for this topic?
|
|
12
|
+
* (looks for handler registrations and tests that trigger them)
|
|
13
|
+
*
|
|
14
|
+
* Operates entirely on source text — no imports, no execution.
|
|
15
|
+
* Output is a list of EventContractGap records suitable for AgentInsightStore.
|
|
16
|
+
*/
|
|
17
|
+
export interface EventContractGap {
|
|
18
|
+
topic: string;
|
|
19
|
+
publishCovered: boolean;
|
|
20
|
+
subscribeCovered: boolean;
|
|
21
|
+
/** Files where the topic is declared */
|
|
22
|
+
declaredIn: string[];
|
|
23
|
+
/** Test files that cover the publish side */
|
|
24
|
+
publishCoveredBy: string[];
|
|
25
|
+
/** Test files that cover the subscribe side */
|
|
26
|
+
subscribeCoveredBy: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface EventContractReport {
|
|
29
|
+
analyzedAt: string;
|
|
30
|
+
totalTopics: number;
|
|
31
|
+
fullyUncovered: number;
|
|
32
|
+
publishOnly: number;
|
|
33
|
+
subscribeOnly: number;
|
|
34
|
+
fullyCovered: number;
|
|
35
|
+
gaps: EventContractGap[];
|
|
36
|
+
}
|
|
37
|
+
export declare function analyzeEventContracts(rootDir: string): Promise<EventContractReport>;
|
|
38
|
+
//# sourceMappingURL=EventContractAnalyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventContractAnalyzer.d.ts","sourceRoot":"","sources":["../src/EventContractAnalyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,wCAAwC;IACxC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,6CAA6C;IAC7C,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,+CAA+C;IAC/C,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,gBAAgB,EAAE,CAAC;CAC1B;AAyCD,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAiEzF"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventContractAnalyzer
|
|
3
|
+
*
|
|
4
|
+
* Static analyzer for event contract coverage gaps.
|
|
5
|
+
*
|
|
6
|
+
* For each topic declared in ORDER_EVENTS (or any `const X = { ... }` pattern
|
|
7
|
+
* matching event topic strings), it checks:
|
|
8
|
+
*
|
|
9
|
+
* 1. PUBLISH side — is there a test that asserts this topic was published?
|
|
10
|
+
* (looks for assertPublished / assertPublishedCount / mockEventBus calls)
|
|
11
|
+
* 2. SUBSCRIBE side — is there a test that exercises the handler for this topic?
|
|
12
|
+
* (looks for handler registrations and tests that trigger them)
|
|
13
|
+
*
|
|
14
|
+
* Operates entirely on source text — no imports, no execution.
|
|
15
|
+
* Output is a list of EventContractGap records suitable for AgentInsightStore.
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { glob } from './glob.js';
|
|
20
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
21
|
+
/** Extract all string values from `const X = { KEY: 'value', ... }` patterns */
|
|
22
|
+
function extractEventTopics(src) {
|
|
23
|
+
const topics = [];
|
|
24
|
+
// Match single or double quoted string values in object literals
|
|
25
|
+
const valuePattern = /:\s*['"]([a-z][a-z0-9]*(?:[._][a-z0-9]+)+)['"]/g;
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = valuePattern.exec(src)) !== null) {
|
|
28
|
+
topics.push(match[1]);
|
|
29
|
+
}
|
|
30
|
+
return [...new Set(topics)];
|
|
31
|
+
}
|
|
32
|
+
/** Check if a file's content references a topic string in a test-assertion context */
|
|
33
|
+
function coversPublish(src, topic) {
|
|
34
|
+
const escaped = topic.replace(/\./g, '\\.');
|
|
35
|
+
// assertPublished / assertPublishedCount / eventBus.publish calls in test files
|
|
36
|
+
const patterns = [
|
|
37
|
+
new RegExp(`assertPublished(?:Count)?\\([^)]*['"]${escaped}['"]`),
|
|
38
|
+
new RegExp(`publish\\([^)]*['"]${escaped}['"]`),
|
|
39
|
+
new RegExp(`ORDER_EVENTS\\.\\w+.*${escaped}`),
|
|
40
|
+
];
|
|
41
|
+
return patterns.some((p) => p.test(src));
|
|
42
|
+
}
|
|
43
|
+
/** Check if a file's content exercises the subscribe handler for a topic */
|
|
44
|
+
function coversSubscribe(src, topic) {
|
|
45
|
+
const escaped = topic.replace(/\./g, '\\.');
|
|
46
|
+
const patterns = [
|
|
47
|
+
new RegExp(`subscribe\\([^)]*['"]${escaped}['"]`),
|
|
48
|
+
new RegExp(`registerEventHandlers`), // integration tests that call this verify all handlers
|
|
49
|
+
new RegExp(`handle.*${escaped.replace(/\\\./g, '.*')}`),
|
|
50
|
+
];
|
|
51
|
+
return patterns.some((p) => p.test(src));
|
|
52
|
+
}
|
|
53
|
+
// ── Main analyzer ──────────────────────────────────────────────────────────
|
|
54
|
+
export async function analyzeEventContracts(rootDir) {
|
|
55
|
+
// 1. Find all source files that declare event topics
|
|
56
|
+
const sourceFiles = await glob(rootDir, ['services/**/*.ts', 'shared/contracts/**/*.ts'], [
|
|
57
|
+
'node_modules', 'dist', '**/*.test.ts', '**/*.spec.ts',
|
|
58
|
+
]);
|
|
59
|
+
// 2. Find all test files
|
|
60
|
+
const testFiles = await glob(rootDir, ['tests/**/*.ts'], ['node_modules', 'dist']);
|
|
61
|
+
// 3. Collect topics from source files
|
|
62
|
+
const topicToFiles = new Map();
|
|
63
|
+
for (const file of sourceFiles) {
|
|
64
|
+
const src = fs.readFileSync(file, 'utf8');
|
|
65
|
+
const topics = extractEventTopics(src);
|
|
66
|
+
for (const topic of topics) {
|
|
67
|
+
const existing = topicToFiles.get(topic) ?? [];
|
|
68
|
+
existing.push(path.relative(rootDir, file));
|
|
69
|
+
topicToFiles.set(topic, existing);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// 4. For each topic, check test coverage
|
|
73
|
+
const gaps = [];
|
|
74
|
+
// Pre-read all test files once
|
|
75
|
+
const testContents = testFiles.map((f) => ({
|
|
76
|
+
relPath: path.relative(rootDir, f),
|
|
77
|
+
src: fs.readFileSync(f, 'utf8'),
|
|
78
|
+
}));
|
|
79
|
+
for (const [topic, declaredIn] of topicToFiles) {
|
|
80
|
+
const publishCoveredBy = testContents
|
|
81
|
+
.filter(({ src }) => coversPublish(src, topic))
|
|
82
|
+
.map(({ relPath }) => relPath);
|
|
83
|
+
const subscribeCoveredBy = testContents
|
|
84
|
+
.filter(({ src }) => coversSubscribe(src, topic))
|
|
85
|
+
.map(({ relPath }) => relPath);
|
|
86
|
+
gaps.push({
|
|
87
|
+
topic,
|
|
88
|
+
publishCovered: publishCoveredBy.length > 0,
|
|
89
|
+
subscribeCovered: subscribeCoveredBy.length > 0,
|
|
90
|
+
declaredIn,
|
|
91
|
+
publishCoveredBy,
|
|
92
|
+
subscribeCoveredBy,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// 5. Sort: fully uncovered first, then partial, then fully covered
|
|
96
|
+
gaps.sort((a, b) => {
|
|
97
|
+
const scoreA = (a.publishCovered ? 1 : 0) + (a.subscribeCovered ? 1 : 0);
|
|
98
|
+
const scoreB = (b.publishCovered ? 1 : 0) + (b.subscribeCovered ? 1 : 0);
|
|
99
|
+
return scoreA - scoreB;
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
analyzedAt: new Date().toISOString(),
|
|
103
|
+
totalTopics: gaps.length,
|
|
104
|
+
fullyUncovered: gaps.filter((g) => !g.publishCovered && !g.subscribeCovered).length,
|
|
105
|
+
publishOnly: gaps.filter((g) => g.publishCovered && !g.subscribeCovered).length,
|
|
106
|
+
subscribeOnly: gaps.filter((g) => !g.publishCovered && g.subscribeCovered).length,
|
|
107
|
+
fullyCovered: gaps.filter((g) => g.publishCovered && g.subscribeCovered).length,
|
|
108
|
+
gaps,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=EventContractAnalyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventContractAnalyzer.js","sourceRoot":"","sources":["../src/EventContractAnalyzer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlakinessAnalyzer
|
|
3
|
+
*
|
|
4
|
+
* Computes FlakeScore per test from a window of TestRunRecords.
|
|
5
|
+
* Pure function — no I/O. Feed it records, get scores back.
|
|
6
|
+
*
|
|
7
|
+
* Flakiness definition:
|
|
8
|
+
* - flakeRate = failCount / totalRuns
|
|
9
|
+
* - isFlaky = 0 < flakeRate < 1 (passes sometimes, fails sometimes)
|
|
10
|
+
* - isAlwaysFailing = flakeRate === 1
|
|
11
|
+
*
|
|
12
|
+
* A test with flakeRate === 0 is healthy and is excluded from the result
|
|
13
|
+
* unless includeHealthy is true.
|
|
14
|
+
*/
|
|
15
|
+
import { TestRunRecord, FlakeScore } from '@horus/contracts';
|
|
16
|
+
export interface FlakinessAnalyzerOptions {
|
|
17
|
+
/** Include tests with flakeRate === 0 in results (default: false) */
|
|
18
|
+
includeHealthy?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function computeFlakeScores(records: TestRunRecord[], options?: FlakinessAnalyzerOptions): FlakeScore[];
|
|
21
|
+
//# sourceMappingURL=FlakinesAnalyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlakinesAnalyzer.d.ts","sourceRoot":"","sources":["../src/FlakinesAnalyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE7D,MAAM,WAAW,wBAAwB;IACvC,qEAAqE;IACrE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,aAAa,EAAE,EACxB,OAAO,GAAE,wBAA6B,GACrC,UAAU,EAAE,CA2Cd"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlakinessAnalyzer
|
|
3
|
+
*
|
|
4
|
+
* Computes FlakeScore per test from a window of TestRunRecords.
|
|
5
|
+
* Pure function — no I/O. Feed it records, get scores back.
|
|
6
|
+
*
|
|
7
|
+
* Flakiness definition:
|
|
8
|
+
* - flakeRate = failCount / totalRuns
|
|
9
|
+
* - isFlaky = 0 < flakeRate < 1 (passes sometimes, fails sometimes)
|
|
10
|
+
* - isAlwaysFailing = flakeRate === 1
|
|
11
|
+
*
|
|
12
|
+
* A test with flakeRate === 0 is healthy and is excluded from the result
|
|
13
|
+
* unless includeHealthy is true.
|
|
14
|
+
*/
|
|
15
|
+
export function computeFlakeScores(records, options = {}) {
|
|
16
|
+
// Group records by test name
|
|
17
|
+
const byTest = new Map();
|
|
18
|
+
for (const record of records) {
|
|
19
|
+
const group = byTest.get(record.testName) ?? [];
|
|
20
|
+
group.push(record);
|
|
21
|
+
byTest.set(record.testName, group);
|
|
22
|
+
}
|
|
23
|
+
const scores = [];
|
|
24
|
+
for (const [testName, testRecords] of byTest) {
|
|
25
|
+
const totalRuns = testRecords.length;
|
|
26
|
+
const passCount = testRecords.filter((r) => r.passed).length;
|
|
27
|
+
const failCount = totalRuns - passCount;
|
|
28
|
+
const flakeRate = totalRuns > 0 ? failCount / totalRuns : 0;
|
|
29
|
+
const avgDurationMs = totalRuns > 0
|
|
30
|
+
? Math.round(testRecords.reduce((sum, r) => sum + r.durationMs, 0) / totalRuns)
|
|
31
|
+
: 0;
|
|
32
|
+
const score = {
|
|
33
|
+
testName,
|
|
34
|
+
layer: testRecords[0].layer,
|
|
35
|
+
totalRuns,
|
|
36
|
+
passCount,
|
|
37
|
+
failCount,
|
|
38
|
+
flakeRate: Math.round(flakeRate * 1000) / 1000,
|
|
39
|
+
isFlaky: flakeRate > 0 && flakeRate < 1,
|
|
40
|
+
isAlwaysFailing: flakeRate === 1,
|
|
41
|
+
avgDurationMs,
|
|
42
|
+
};
|
|
43
|
+
if (options.includeHealthy || score.isFlaky || score.isAlwaysFailing) {
|
|
44
|
+
scores.push(score);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Sort: always-failing first, then by flake rate descending
|
|
48
|
+
return scores.sort((a, b) => {
|
|
49
|
+
if (a.isAlwaysFailing !== b.isAlwaysFailing)
|
|
50
|
+
return a.isAlwaysFailing ? -1 : 1;
|
|
51
|
+
return b.flakeRate - a.flakeRate;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=FlakinesAnalyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlakinesAnalyzer.js","sourceRoot":"","sources":["../src/FlakinesAnalyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AASH,MAAM,UAAU,kBAAkB,CAChC,OAAwB,EACxB,UAAoC,EAAE;IAEtC,6BAA6B;IAC7B,MAAM,MAAM,GAAG,IAAI,GAAG,EAA2B,CAAC;IAClD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnB,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACrC,CAAC;IAED,MAAM,MAAM,GAAiB,EAAE,CAAC;IAEhC,KAAK,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,IAAI,MAAM,EAAE,CAAC;QAC7C,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC;QACrC,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;QAC7D,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;QACxC,MAAM,SAAS,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5D,MAAM,aAAa,GACjB,SAAS,GAAG,CAAC;YACX,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC;YAC/E,CAAC,CAAC,CAAC,CAAC;QAER,MAAM,KAAK,GAAe;YACxB,QAAQ;YACR,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,KAAK;YAC3B,SAAS;YACT,SAAS;YACT,SAAS;YACT,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,IAAI;YAC9C,OAAO,EAAE,SAAS,GAAG,CAAC,IAAI,SAAS,GAAG,CAAC;YACvC,eAAe,EAAE,SAAS,KAAK,CAAC;YAChC,aAAa;SACd,CAAC;QAEF,IAAI,OAAO,CAAC,cAAc,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,eAAe,EAAE,CAAC;YACrE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,4DAA4D;IAC5D,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC1B,IAAI,CAAC,CAAC,eAAe,KAAK,CAAC,CAAC,eAAe;YAAE,OAAO,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/E,OAAO,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HorusVitestReporter
|
|
3
|
+
*
|
|
4
|
+
* A custom Vitest reporter that writes a TestRunRecord to the TestRunStore
|
|
5
|
+
* for every test result. Plugs into vitest.config.ts via the `reporters` array.
|
|
6
|
+
*
|
|
7
|
+
* Usage in vitest.config.ts:
|
|
8
|
+
* import { HorusVitestReporter } from '@horus/insight-store';
|
|
9
|
+
* reporters: ['default', new HorusVitestReporter('./reports')]
|
|
10
|
+
*
|
|
11
|
+
* Records land in: reports/test-runs/<layer>.jsonl
|
|
12
|
+
*/
|
|
13
|
+
import type { Reporter, TestCase } from 'vitest/node';
|
|
14
|
+
import { HorusConfig } from '@horus/contracts';
|
|
15
|
+
export declare class HorusVitestReporter implements Reporter {
|
|
16
|
+
private readonly store;
|
|
17
|
+
private readonly commitSha;
|
|
18
|
+
constructor(config: HorusConfig | string);
|
|
19
|
+
onTestCaseResult(testCase: TestCase): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=HorusVitestReporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HorusVitestReporter.d.ts","sourceRoot":"","sources":["../src/HorusVitestReporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAiB,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAY9D,qBAAa,mBAAoB,YAAW,QAAQ;IAClD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,MAAM,EAAE,WAAW,GAAG,MAAM;IASlC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CAmB1D"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HorusVitestReporter
|
|
3
|
+
*
|
|
4
|
+
* A custom Vitest reporter that writes a TestRunRecord to the TestRunStore
|
|
5
|
+
* for every test result. Plugs into vitest.config.ts via the `reporters` array.
|
|
6
|
+
*
|
|
7
|
+
* Usage in vitest.config.ts:
|
|
8
|
+
* import { HorusVitestReporter } from '@horus/insight-store';
|
|
9
|
+
* reporters: ['default', new HorusVitestReporter('./reports')]
|
|
10
|
+
*
|
|
11
|
+
* Records land in: reports/test-runs/<layer>.jsonl
|
|
12
|
+
*/
|
|
13
|
+
import { TestRunStore } from './TestRunStore.js';
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
/** Infer layer from the test file path */
|
|
17
|
+
function inferLayer(filepath) {
|
|
18
|
+
const normalized = filepath.replace(/\\/g, '/');
|
|
19
|
+
if (normalized.includes('/e2e/'))
|
|
20
|
+
return 'e2e';
|
|
21
|
+
if (normalized.includes('/integration/'))
|
|
22
|
+
return 'integration';
|
|
23
|
+
return 'unit';
|
|
24
|
+
}
|
|
25
|
+
export class HorusVitestReporter {
|
|
26
|
+
store;
|
|
27
|
+
commitSha;
|
|
28
|
+
constructor(config) {
|
|
29
|
+
const reportsDir = typeof config === 'string' ? config : config.reportsDir;
|
|
30
|
+
this.commitSha =
|
|
31
|
+
(typeof config !== 'string' ? config.commitSha : undefined) ??
|
|
32
|
+
process.env.GITHUB_SHA ??
|
|
33
|
+
'local';
|
|
34
|
+
this.store = new TestRunStore(path.resolve(reportsDir));
|
|
35
|
+
}
|
|
36
|
+
async onTestCaseResult(testCase) {
|
|
37
|
+
const result = testCase.result();
|
|
38
|
+
if (!result)
|
|
39
|
+
return;
|
|
40
|
+
const diagnostic = testCase.diagnostic();
|
|
41
|
+
const record = {
|
|
42
|
+
id: crypto.randomUUID(),
|
|
43
|
+
testName: testCase.fullName,
|
|
44
|
+
layer: inferLayer(testCase.module.moduleId),
|
|
45
|
+
runAt: new Date().toISOString(),
|
|
46
|
+
passed: result.state === 'passed',
|
|
47
|
+
durationMs: Math.round(diagnostic?.duration ?? 0),
|
|
48
|
+
retries: diagnostic?.retryCount ?? 0,
|
|
49
|
+
commitSha: this.commitSha,
|
|
50
|
+
};
|
|
51
|
+
await this.store.append(record);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=HorusVitestReporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HorusVitestReporter.js","sourceRoot":"","sources":["../src/HorusVitestReporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,0CAA0C;AAC1C,SAAS,UAAU,CAAC,QAAgB;IAClC,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAChD,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/C,IAAI,UAAU,CAAC,QAAQ,CAAC,eAAe,CAAC;QAAE,OAAO,aAAa,CAAC;IAC/D,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,OAAO,mBAAmB;IACb,KAAK,CAAe;IACpB,SAAS,CAAS;IAEnC,YAAY,MAA4B;QACtC,MAAM,UAAU,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC;QAC3E,IAAI,CAAC,SAAS;YACZ,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;gBAC3D,OAAO,CAAC,GAAG,CAAC,UAAU;gBACtB,OAAO,CAAC;QACV,IAAI,CAAC,KAAK,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,QAAkB;QACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;QACjC,IAAI,CAAC,MAAM;YAAE,OAAO;QAEpB,MAAM,UAAU,GAAG,QAAQ,CAAC,UAAU,EAAE,CAAC;QAEzC,MAAM,MAAM,GAAkB;YAC5B,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE;YACvB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;YAC3B,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC;YAC3C,KAAK,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC/B,MAAM,EAAE,MAAM,CAAC,KAAK,KAAK,QAAQ;YACjC,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,QAAQ,IAAI,CAAC,CAAC;YACjD,OAAO,EAAE,UAAU,EAAE,UAAU,IAAI,CAAC;YACpC,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC;QAEF,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;CACF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestRunStore
|
|
3
|
+
*
|
|
4
|
+
* JSONL-backed persistence for individual test run records.
|
|
5
|
+
* Files: reports/test-runs/<layer>.jsonl — one record per line per test per CI run.
|
|
6
|
+
*
|
|
7
|
+
* Keeping records by layer (unit/integration/e2e) keeps files small and allows
|
|
8
|
+
* layer-specific queries without scanning everything.
|
|
9
|
+
*/
|
|
10
|
+
import { TestRunRecord, ITestRunStore, HorusConfig } from '@horus/contracts';
|
|
11
|
+
export declare class TestRunStore implements ITestRunStore {
|
|
12
|
+
private readonly dir;
|
|
13
|
+
constructor(config: HorusConfig | string);
|
|
14
|
+
append(record: TestRunRecord): Promise<void>;
|
|
15
|
+
readAll(): Promise<TestRunRecord[]>;
|
|
16
|
+
readSince(isoTimestamp: string): Promise<TestRunRecord[]>;
|
|
17
|
+
readByTest(testName: string): Promise<TestRunRecord[]>;
|
|
18
|
+
private readFile;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=TestRunStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TestRunStore.d.ts","sourceRoot":"","sources":["../src/TestRunStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE7E,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,MAAM,EAAE,WAAW,GAAG,MAAM;IAMlC,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAK5C,OAAO,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAYnC,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAKzD,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAO5D,OAAO,CAAC,QAAQ;CAQjB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestRunStore
|
|
3
|
+
*
|
|
4
|
+
* JSONL-backed persistence for individual test run records.
|
|
5
|
+
* Files: reports/test-runs/<layer>.jsonl — one record per line per test per CI run.
|
|
6
|
+
*
|
|
7
|
+
* Keeping records by layer (unit/integration/e2e) keeps files small and allows
|
|
8
|
+
* layer-specific queries without scanning everything.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
export class TestRunStore {
|
|
13
|
+
dir;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
const reportsDir = typeof config === 'string' ? config : config.reportsDir;
|
|
16
|
+
this.dir = path.resolve(reportsDir, 'test-runs');
|
|
17
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
async append(record) {
|
|
20
|
+
const filePath = path.join(this.dir, `${record.layer}.jsonl`);
|
|
21
|
+
fs.appendFileSync(filePath, JSON.stringify(record) + '\n', 'utf8');
|
|
22
|
+
}
|
|
23
|
+
async readAll() {
|
|
24
|
+
const files = fs.existsSync(this.dir)
|
|
25
|
+
? fs.readdirSync(this.dir).filter((f) => f.endsWith('.jsonl'))
|
|
26
|
+
: [];
|
|
27
|
+
const all = [];
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
all.push(...this.readFile(path.join(this.dir, file)));
|
|
30
|
+
}
|
|
31
|
+
return all.sort((a, b) => a.runAt.localeCompare(b.runAt));
|
|
32
|
+
}
|
|
33
|
+
async readSince(isoTimestamp) {
|
|
34
|
+
const all = await this.readAll();
|
|
35
|
+
return all.filter((r) => r.runAt >= isoTimestamp);
|
|
36
|
+
}
|
|
37
|
+
async readByTest(testName) {
|
|
38
|
+
const all = await this.readAll();
|
|
39
|
+
return all.filter((r) => r.testName === testName);
|
|
40
|
+
}
|
|
41
|
+
// ── Private helpers ──────────────────────────────────────────────────────
|
|
42
|
+
readFile(filePath) {
|
|
43
|
+
if (!fs.existsSync(filePath))
|
|
44
|
+
return [];
|
|
45
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
46
|
+
return raw
|
|
47
|
+
.split('\n')
|
|
48
|
+
.filter((line) => line.trim().length > 0)
|
|
49
|
+
.map((line) => JSON.parse(line));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=TestRunStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TestRunStore.js","sourceRoot":"","sources":["../src/TestRunStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,MAAM,OAAO,YAAY;IACN,GAAG,CAAS;IAE7B,YAAY,MAA4B;QACtC,MAAM,UAAU,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC;QAC3E,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QACjD,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,MAAqB;QAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,QAAQ,CAAC,CAAC;QAC9D,EAAE,CAAC,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IACrE,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,KAAK,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC;YACnC,CAAC,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAC9D,CAAC,CAAC,EAAE,CAAC;QAEP,MAAM,GAAG,GAAoB,EAAE,CAAC;QAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QACxD,CAAC;QACD,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,YAAoB;QAClC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACjC,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,YAAY,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAAgB;QAC/B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACjC,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC;IACpD,CAAC;IAED,4EAA4E;IAEpE,QAAQ,CAAC,QAAgB;QAC/B,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC9C,OAAO,GAAG;aACP,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;aACxC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAkB,CAAC,CAAC;IACtD,CAAC;CACF"}
|
package/dist/glob.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal glob utility — no external dependencies.
|
|
3
|
+
* Supports patterns like 'services/**\/*.ts' and an exclude list of directory/file prefixes.
|
|
4
|
+
*/
|
|
5
|
+
export declare function glob(rootDir: string, patterns: string[], excludes?: string[]): Promise<string[]>;
|
|
6
|
+
//# sourceMappingURL=glob.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,wBAAsB,IAAI,CACxB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAAE,EAClB,QAAQ,GAAE,MAAM,EAAO,GACtB,OAAO,CAAC,MAAM,EAAE,CAAC,CAOnB"}
|
package/dist/glob.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal glob utility — no external dependencies.
|
|
3
|
+
* Supports patterns like 'services/**\/*.ts' and an exclude list of directory/file prefixes.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
export async function glob(rootDir, patterns, excludes = []) {
|
|
8
|
+
const results = [];
|
|
9
|
+
const compiledPatterns = patterns.map(compileGlob);
|
|
10
|
+
const compiledExcludes = excludes.map((e) => new RegExp(e.replace(/\*/g, '[^/]*')));
|
|
11
|
+
walk(rootDir, rootDir, compiledPatterns, compiledExcludes, results);
|
|
12
|
+
return results;
|
|
13
|
+
}
|
|
14
|
+
function walk(rootDir, dir, patterns, excludes, results) {
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const abs = path.join(dir, entry.name);
|
|
24
|
+
const rel = path.relative(rootDir, abs).replace(/\\/g, '/');
|
|
25
|
+
if (excludes.some((ex) => ex.test(rel) || ex.test(entry.name)))
|
|
26
|
+
continue;
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
walk(rootDir, abs, patterns, excludes, results);
|
|
29
|
+
}
|
|
30
|
+
else if (entry.isFile()) {
|
|
31
|
+
if (patterns.some((p) => p.test(rel))) {
|
|
32
|
+
results.push(abs);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function compileGlob(pattern) {
|
|
38
|
+
const escaped = pattern
|
|
39
|
+
.replace(/\./g, '\\.')
|
|
40
|
+
.replace(/\*\*\//g, '(.+/)?')
|
|
41
|
+
.replace(/\*/g, '[^/]*');
|
|
42
|
+
return new RegExp(`^${escaped}$`);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=glob.js.map
|
package/dist/glob.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,CAAC,KAAK,UAAU,IAAI,CACxB,OAAe,EACf,QAAkB,EAClB,WAAqB,EAAE;IAEvB,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,gBAAgB,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACnD,MAAM,gBAAgB,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAEpF,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,OAAO,CAAC,CAAC;IACpE,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,IAAI,CACX,OAAe,EACf,GAAW,EACX,QAAkB,EAClB,QAAkB,EAClB,OAAiB;IAEjB,IAAI,OAAoB,CAAC;IACzB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;IACT,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACvC,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAE5D,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAAE,SAAS;QAEzE,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QAClD,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YAC1B,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBACtC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,OAAe;IAClC,MAAM,OAAO,GAAG,OAAO;SACpB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACrB,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC;SAC5B,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC3B,OAAO,IAAI,MAAM,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC;AACpC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { AgentInsightStore } from './AgentInsightStore.js';
|
|
2
|
+
export { TestRunStore } from './TestRunStore.js';
|
|
3
|
+
export { HorusVitestReporter } from './HorusVitestReporter.js';
|
|
4
|
+
export { computeFlakeScores } from './FlakinesAnalyzer.js';
|
|
5
|
+
export type { FlakinessAnalyzerOptions } from './FlakinesAnalyzer.js';
|
|
6
|
+
export { CoverageStore, computeDelta } from './CoverageStore.js';
|
|
7
|
+
export { analyzeEventContracts } from './EventContractAnalyzer.js';
|
|
8
|
+
export type { EventContractGap, EventContractReport } from './EventContractAnalyzer.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,YAAY,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { AgentInsightStore } from './AgentInsightStore.js';
|
|
2
|
+
export { TestRunStore } from './TestRunStore.js';
|
|
3
|
+
export { HorusVitestReporter } from './HorusVitestReporter.js';
|
|
4
|
+
export { computeFlakeScores } from './FlakinesAnalyzer.js';
|
|
5
|
+
export { CoverageStore, computeDelta } from './CoverageStore.js';
|
|
6
|
+
export { analyzeEventContracts } from './EventContractAnalyzer.js';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC"}
|
package/dist/ingest.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* horus-ingest
|
|
4
|
+
*
|
|
5
|
+
* Reads a Vitest or Jest JSON reporter output file and appends a TestRunRecord
|
|
6
|
+
* to the TestRunStore for each test result.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* horus-ingest --file reports/unit-results.json --layer unit
|
|
10
|
+
* horus-ingest --file reports/integration-results.json --layer integration
|
|
11
|
+
*
|
|
12
|
+
* Compatible with: Vitest (--reporter=json), Jest (--json)
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=ingest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../src/ingest.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;GAWG"}
|
package/dist/ingest.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* horus-ingest
|
|
4
|
+
*
|
|
5
|
+
* Reads a Vitest or Jest JSON reporter output file and appends a TestRunRecord
|
|
6
|
+
* to the TestRunStore for each test result.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* horus-ingest --file reports/unit-results.json --layer unit
|
|
10
|
+
* horus-ingest --file reports/integration-results.json --layer integration
|
|
11
|
+
*
|
|
12
|
+
* Compatible with: Vitest (--reporter=json), Jest (--json)
|
|
13
|
+
*/
|
|
14
|
+
import { TestRunStore } from './TestRunStore.js';
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { parseArgs } from 'node:util';
|
|
17
|
+
import crypto from 'node:crypto';
|
|
18
|
+
const { values } = parseArgs({
|
|
19
|
+
options: {
|
|
20
|
+
file: { type: 'string' },
|
|
21
|
+
layer: { type: 'string' },
|
|
22
|
+
reportsDir: { type: 'string', default: './reports' },
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
if (!values.file) {
|
|
26
|
+
console.error('Error: --file is required');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const raw = JSON.parse(readFileSync(values.file, 'utf8'));
|
|
30
|
+
const store = new TestRunStore({ reportsDir: values.reportsDir });
|
|
31
|
+
const commitSha = process.env.GITHUB_SHA ?? 'local';
|
|
32
|
+
const layer = (values.layer ?? 'unit');
|
|
33
|
+
// Normalize Vitest and Jest JSON shapes to TestRunRecord
|
|
34
|
+
const tests = raw.testResults // Jest shape
|
|
35
|
+
?? raw.files?.flatMap((f) => f.tests ?? []) // Vitest shape
|
|
36
|
+
?? [];
|
|
37
|
+
for (const t of tests) {
|
|
38
|
+
const record = {
|
|
39
|
+
id: crypto.randomUUID(),
|
|
40
|
+
testName: t.fullName ?? t.name ?? 'unknown',
|
|
41
|
+
layer,
|
|
42
|
+
runAt: new Date().toISOString(),
|
|
43
|
+
passed: (t.status ?? t.state) === 'passed',
|
|
44
|
+
durationMs: Math.round(t.duration ?? 0),
|
|
45
|
+
retries: t.retryCount ?? 0,
|
|
46
|
+
commitSha,
|
|
47
|
+
};
|
|
48
|
+
await store.append(record);
|
|
49
|
+
}
|
|
50
|
+
console.log(`Ingested ${tests.length} test records → ${values.reportsDir}/test-runs/${layer}.jsonl`);
|
|
51
|
+
//# sourceMappingURL=ingest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest.js","sourceRoot":"","sources":["../src/ingest.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE;QACP,IAAI,EAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE;QAC9B,KAAK,EAAO,EAAE,IAAI,EAAE,QAAQ,EAAE;QAC9B,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE;KACrD;CACF,CAAC,CAAC;AAEH,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACjB,OAAO,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAC3C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;AAC1D,MAAM,KAAK,GAAG,IAAI,YAAY,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAW,EAAE,CAAC,CAAC;AACnE,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC;AACpD,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAA2B,CAAC;AAEjE,yDAAyD;AACzD,MAAM,KAAK,GACT,GAAG,CAAC,WAAW,CAAe,aAAa;OACxC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAwB,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,eAAe;OAC/E,EAAE,CAAC;AAER,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;IACtB,MAAM,MAAM,GAAkB;QAC5B,EAAE,EAAU,MAAM,CAAC,UAAU,EAAE;QAC/B,QAAQ,EAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,IAAI,SAAS;QAC7C,KAAK;QACL,KAAK,EAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACpC,MAAM,EAAM,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,QAAQ;QAC9C,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC;QACvC,OAAO,EAAK,CAAC,CAAC,UAAU,IAAI,CAAC;QAC7B,SAAS;KACV,CAAC;IACF,MAAM,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAC7B,CAAC;AAED,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,CAAC,MAAM,mBAAmB,MAAM,CAAC,UAAU,cAAc,KAAK,QAAQ,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wutangbanger/horus-insight-store",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "JSONL-backed persistence for AI agent insights",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@wutangbanger/horus-contracts": "1.0.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"vitest": ">=1.0.0 <5.0.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependenciesMeta": {
|
|
27
|
+
"vitest": {
|
|
28
|
+
"optional": true
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"bin": {
|
|
32
|
+
"horus-ingest": "./dist/ingest.js"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"clean": "rm -rf dist"
|
|
37
|
+
}
|
|
38
|
+
}
|