@rigour-labs/core 2.22.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/context.test.js +2 -3
- package/dist/environment.test.js +2 -1
- package/dist/gates/agent-team.d.ts +2 -1
- package/dist/gates/agent-team.js +1 -0
- package/dist/gates/base.d.ts +3 -1
- package/dist/gates/base.js +3 -0
- package/dist/gates/checkpoint.d.ts +2 -1
- package/dist/gates/checkpoint.js +3 -2
- package/dist/gates/context-window-artifacts.d.ts +2 -1
- package/dist/gates/context-window-artifacts.js +6 -3
- package/dist/gates/context.d.ts +2 -1
- package/dist/gates/context.js +1 -0
- package/dist/gates/coverage.js +3 -1
- package/dist/gates/dependency.js +5 -5
- package/dist/gates/duplication-drift.d.ts +2 -1
- package/dist/gates/duplication-drift.js +4 -1
- package/dist/gates/environment.js +4 -4
- package/dist/gates/hallucinated-imports.d.ts +21 -2
- package/dist/gates/hallucinated-imports.js +116 -2
- package/dist/gates/inconsistent-error-handling.d.ts +2 -1
- package/dist/gates/inconsistent-error-handling.js +21 -7
- package/dist/gates/promise-safety.d.ts +68 -0
- package/dist/gates/promise-safety.js +509 -0
- package/dist/gates/retry-loop-breaker.d.ts +2 -1
- package/dist/gates/retry-loop-breaker.js +2 -1
- package/dist/gates/runner.js +34 -1
- package/dist/gates/safety.d.ts +2 -1
- package/dist/gates/safety.js +2 -1
- package/dist/gates/security-patterns-owasp.test.d.ts +1 -0
- package/dist/gates/security-patterns-owasp.test.js +171 -0
- package/dist/gates/security-patterns.d.ts +6 -1
- package/dist/gates/security-patterns.js +101 -0
- package/dist/gates/structure.js +1 -1
- package/dist/hooks/checker.d.ts +23 -0
- package/dist/hooks/checker.js +222 -0
- package/dist/hooks/checker.test.d.ts +1 -0
- package/dist/hooks/checker.test.js +132 -0
- package/dist/hooks/index.d.ts +9 -0
- package/dist/hooks/index.js +8 -0
- package/dist/hooks/standalone-checker.d.ts +15 -0
- package/dist/hooks/standalone-checker.js +106 -0
- package/dist/hooks/templates.d.ts +22 -0
- package/dist/hooks/templates.js +232 -0
- package/dist/hooks/types.d.ts +34 -0
- package/dist/hooks/types.js +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/services/fix-packet-service.d.ts +0 -1
- package/dist/services/fix-packet-service.js +9 -14
- package/dist/services/score-history.d.ts +54 -0
- package/dist/services/score-history.js +122 -0
- package/dist/templates/index.js +176 -0
- package/dist/types/fix-packet.d.ts +5 -5
- package/dist/types/fix-packet.js +1 -1
- package/dist/types/index.d.ts +207 -0
- package/dist/types/index.js +32 -0
- package/package.json +21 -1
- package/src/context.test.ts +0 -256
- package/src/discovery.test.ts +0 -88
- package/src/discovery.ts +0 -112
- package/src/environment.test.ts +0 -115
- package/src/gates/agent-team.test.ts +0 -134
- package/src/gates/agent-team.ts +0 -210
- package/src/gates/ast-handlers/base.ts +0 -13
- package/src/gates/ast-handlers/python.ts +0 -145
- package/src/gates/ast-handlers/python_parser.py +0 -181
- package/src/gates/ast-handlers/typescript.ts +0 -264
- package/src/gates/ast-handlers/universal.ts +0 -184
- package/src/gates/ast.ts +0 -54
- package/src/gates/base.ts +0 -28
- package/src/gates/checkpoint.test.ts +0 -135
- package/src/gates/checkpoint.ts +0 -311
- package/src/gates/content.ts +0 -51
- package/src/gates/context-window-artifacts.ts +0 -277
- package/src/gates/context.ts +0 -270
- package/src/gates/coverage.ts +0 -74
- package/src/gates/dependency.ts +0 -108
- package/src/gates/duplication-drift.ts +0 -231
- package/src/gates/environment.ts +0 -94
- package/src/gates/file.ts +0 -46
- package/src/gates/hallucinated-imports.ts +0 -361
- package/src/gates/inconsistent-error-handling.ts +0 -254
- package/src/gates/retry-loop-breaker.ts +0 -151
- package/src/gates/runner.ts +0 -188
- package/src/gates/safety.ts +0 -56
- package/src/gates/security-patterns.test.ts +0 -162
- package/src/gates/security-patterns.ts +0 -306
- package/src/gates/structure.ts +0 -36
- package/src/index.ts +0 -13
- package/src/pattern-index/embeddings.ts +0 -84
- package/src/pattern-index/index.ts +0 -59
- package/src/pattern-index/indexer.test.ts +0 -276
- package/src/pattern-index/indexer.ts +0 -1023
- package/src/pattern-index/matcher.test.ts +0 -293
- package/src/pattern-index/matcher.ts +0 -493
- package/src/pattern-index/overrides.ts +0 -235
- package/src/pattern-index/security.ts +0 -151
- package/src/pattern-index/staleness.test.ts +0 -313
- package/src/pattern-index/staleness.ts +0 -568
- package/src/pattern-index/types.ts +0 -339
- package/src/safety.test.ts +0 -53
- package/src/services/adaptive-thresholds.test.ts +0 -189
- package/src/services/adaptive-thresholds.ts +0 -275
- package/src/services/context-engine.ts +0 -104
- package/src/services/fix-packet-service.ts +0 -42
- package/src/services/state-service.ts +0 -138
- package/src/smoke.test.ts +0 -18
- package/src/templates/index.ts +0 -338
- package/src/types/fix-packet.ts +0 -32
- package/src/types/index.ts +0 -200
- package/src/utils/logger.ts +0 -43
- package/src/utils/scanner.test.ts +0 -37
- package/src/utils/scanner.ts +0 -43
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -7
- package/vitest.setup.ts +0 -30
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook configuration templates for each AI coding tool.
|
|
3
|
+
*
|
|
4
|
+
* Each template generates the tool-native config format:
|
|
5
|
+
* - Claude Code: .claude/settings.json (PostToolUse matcher)
|
|
6
|
+
* - Cursor: .cursor/hooks.json (afterFileEdit event)
|
|
7
|
+
* - Cline: .clinerules/hooks/PostToolUse (executable script)
|
|
8
|
+
* - Windsurf: .windsurf/hooks.json (post_write_code event)
|
|
9
|
+
*
|
|
10
|
+
* @since v3.0.0
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Generate hook config files for a specific tool.
|
|
14
|
+
*/
|
|
15
|
+
export function generateHookFiles(tool, checkerPath) {
|
|
16
|
+
switch (tool) {
|
|
17
|
+
case 'claude':
|
|
18
|
+
return generateClaudeHooks(checkerPath);
|
|
19
|
+
case 'cursor':
|
|
20
|
+
return generateCursorHooks(checkerPath);
|
|
21
|
+
case 'cline':
|
|
22
|
+
return generateClineHooks(checkerPath);
|
|
23
|
+
case 'windsurf':
|
|
24
|
+
return generateWindsurfHooks(checkerPath);
|
|
25
|
+
default:
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function generateClaudeHooks(checkerPath) {
|
|
30
|
+
const settings = {
|
|
31
|
+
hooks: {
|
|
32
|
+
PostToolUse: [
|
|
33
|
+
{
|
|
34
|
+
matcher: "Write|Edit|MultiEdit",
|
|
35
|
+
hooks: [
|
|
36
|
+
{
|
|
37
|
+
type: "command",
|
|
38
|
+
command: `node ${checkerPath} --files "$TOOL_INPUT_file_path"`,
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
path: '.claude/settings.json',
|
|
48
|
+
content: JSON.stringify(settings, null, 4),
|
|
49
|
+
description: 'Claude Code PostToolUse hook — runs Rigour fast-check after every Write/Edit',
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
function generateCursorHooks(checkerPath) {
|
|
54
|
+
const hooks = {
|
|
55
|
+
version: 1,
|
|
56
|
+
hooks: {
|
|
57
|
+
afterFileEdit: [
|
|
58
|
+
{
|
|
59
|
+
command: `node ${checkerPath} --stdin`,
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const wrapper = `#!/usr/bin/env node
|
|
65
|
+
/**
|
|
66
|
+
* Cursor afterFileEdit hook wrapper for Rigour.
|
|
67
|
+
* Receives { file_path, old_content, new_content } on stdin.
|
|
68
|
+
* Runs Rigour fast-check on the edited file.
|
|
69
|
+
*/
|
|
70
|
+
const { runHookChecker } = require('./node_modules/@rigour-labs/core/dist/hooks/checker.js');
|
|
71
|
+
|
|
72
|
+
let data = '';
|
|
73
|
+
process.stdin.on('data', chunk => { data += chunk; });
|
|
74
|
+
process.stdin.on('end', async () => {
|
|
75
|
+
try {
|
|
76
|
+
const payload = JSON.parse(data);
|
|
77
|
+
const result = await runHookChecker({
|
|
78
|
+
cwd: process.cwd(),
|
|
79
|
+
files: [payload.file_path],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Write result to stdout for Cursor to consume
|
|
83
|
+
process.stdout.write(JSON.stringify({ status: 'ok' }));
|
|
84
|
+
|
|
85
|
+
// Log failures to stderr (visible in Cursor Hooks panel)
|
|
86
|
+
if (result.status === 'fail') {
|
|
87
|
+
for (const f of result.failures) {
|
|
88
|
+
const loc = f.line ? \`:\${f.line}\` : '';
|
|
89
|
+
process.stderr.write(\`[rigour/\${f.gate}] \${f.file}\${loc}: \${f.message}\\n\`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
process.stderr.write(\`Rigour hook error: \${err.message}\\n\`);
|
|
94
|
+
process.stdout.write(JSON.stringify({ status: 'ok' }));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
`;
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
path: '.cursor/hooks.json',
|
|
101
|
+
content: JSON.stringify(hooks, null, 4),
|
|
102
|
+
description: 'Cursor afterFileEdit hook config',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
path: '.cursor/rigour-hook.js',
|
|
106
|
+
content: wrapper,
|
|
107
|
+
executable: true,
|
|
108
|
+
description: 'Cursor hook wrapper that reads stdin and runs Rigour checker',
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
function generateClineHooks(checkerPath) {
|
|
113
|
+
const script = `#!/usr/bin/env node
|
|
114
|
+
/**
|
|
115
|
+
* Cline PostToolUse hook for Rigour.
|
|
116
|
+
* Receives JSON on stdin with { toolName, toolInput, toolOutput }.
|
|
117
|
+
* Only triggers on write_to_file and replace_in_file tools.
|
|
118
|
+
*/
|
|
119
|
+
const { runHookChecker } = require('./node_modules/@rigour-labs/core/dist/hooks/checker.js');
|
|
120
|
+
|
|
121
|
+
const WRITE_TOOLS = ['write_to_file', 'replace_in_file'];
|
|
122
|
+
|
|
123
|
+
let data = '';
|
|
124
|
+
process.stdin.on('data', chunk => { data += chunk; });
|
|
125
|
+
process.stdin.on('end', async () => {
|
|
126
|
+
try {
|
|
127
|
+
const payload = JSON.parse(data);
|
|
128
|
+
|
|
129
|
+
if (!WRITE_TOOLS.includes(payload.toolName)) {
|
|
130
|
+
// Not a write tool, pass through
|
|
131
|
+
process.stdout.write(JSON.stringify({}));
|
|
132
|
+
process.exit(0);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const filePath = payload.toolInput?.path || payload.toolInput?.file_path;
|
|
137
|
+
if (!filePath) {
|
|
138
|
+
process.stdout.write(JSON.stringify({}));
|
|
139
|
+
process.exit(0);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const result = await runHookChecker({
|
|
144
|
+
cwd: process.cwd(),
|
|
145
|
+
files: [filePath],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (result.status === 'fail') {
|
|
149
|
+
const messages = result.failures
|
|
150
|
+
.map(f => {
|
|
151
|
+
const loc = f.line ? \`:\${f.line}\` : '';
|
|
152
|
+
return \`[rigour/\${f.gate}] \${f.file}\${loc}: \${f.message}\`;
|
|
153
|
+
})
|
|
154
|
+
.join('\\n');
|
|
155
|
+
|
|
156
|
+
process.stdout.write(JSON.stringify({
|
|
157
|
+
contextModification: \`\\n[Rigour Quality Gate] Found \${result.failures.length} issue(s):\\n\${messages}\\nPlease fix before continuing.\`,
|
|
158
|
+
}));
|
|
159
|
+
} else {
|
|
160
|
+
process.stdout.write(JSON.stringify({}));
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
process.stderr.write(\`Rigour hook error: \${err.message}\\n\`);
|
|
164
|
+
process.stdout.write(JSON.stringify({}));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
`;
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
path: '.clinerules/hooks/PostToolUse',
|
|
171
|
+
content: script,
|
|
172
|
+
executable: true,
|
|
173
|
+
description: 'Cline PostToolUse hook — runs Rigour fast-check after file writes',
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
function generateWindsurfHooks(checkerPath) {
|
|
178
|
+
const hooks = {
|
|
179
|
+
version: 1,
|
|
180
|
+
hooks: {
|
|
181
|
+
post_write_code: [
|
|
182
|
+
{
|
|
183
|
+
command: `node ${checkerPath} --stdin`,
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const wrapper = `#!/usr/bin/env node
|
|
189
|
+
/**
|
|
190
|
+
* Windsurf post_write_code hook wrapper for Rigour.
|
|
191
|
+
* Receives { file_path, content } on stdin from Cascade agent.
|
|
192
|
+
* Runs Rigour fast-check on the written file.
|
|
193
|
+
*/
|
|
194
|
+
const { runHookChecker } = require('./node_modules/@rigour-labs/core/dist/hooks/checker.js');
|
|
195
|
+
|
|
196
|
+
let data = '';
|
|
197
|
+
process.stdin.on('data', chunk => { data += chunk; });
|
|
198
|
+
process.stdin.on('end', async () => {
|
|
199
|
+
try {
|
|
200
|
+
const payload = JSON.parse(data);
|
|
201
|
+
const result = await runHookChecker({
|
|
202
|
+
cwd: process.cwd(),
|
|
203
|
+
files: [payload.file_path],
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (result.status === 'fail') {
|
|
207
|
+
for (const f of result.failures) {
|
|
208
|
+
const loc = f.line ? \`:\${f.line}\` : '';
|
|
209
|
+
process.stderr.write(\`[rigour/\${f.gate}] \${f.file}\${loc}: \${f.message}\\n\`);
|
|
210
|
+
}
|
|
211
|
+
// Exit 2 = block (if configured), exit 0 = warn only
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
process.stderr.write(\`Rigour hook error: \${err.message}\\n\`);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
`;
|
|
219
|
+
return [
|
|
220
|
+
{
|
|
221
|
+
path: '.windsurf/hooks.json',
|
|
222
|
+
content: JSON.stringify(hooks, null, 4),
|
|
223
|
+
description: 'Windsurf post_write_code hook config',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
path: '.windsurf/rigour-hook.js',
|
|
227
|
+
content: wrapper,
|
|
228
|
+
executable: true,
|
|
229
|
+
description: 'Windsurf hook wrapper that reads stdin and runs Rigour checker',
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook system types for multi-tool integration.
|
|
3
|
+
*
|
|
4
|
+
* Each AI coding tool (Claude Code, Cursor, Cline, Windsurf)
|
|
5
|
+
* has its own hook format. These types unify the config generation.
|
|
6
|
+
*
|
|
7
|
+
* @since v3.0.0
|
|
8
|
+
*/
|
|
9
|
+
export type HookTool = 'claude' | 'cursor' | 'cline' | 'windsurf';
|
|
10
|
+
export interface HookConfig {
|
|
11
|
+
/** Which tools to generate hooks for */
|
|
12
|
+
tools: HookTool[];
|
|
13
|
+
/** Gates to run in the hook checker (fast subset) */
|
|
14
|
+
fast_gates: string[];
|
|
15
|
+
/** Max execution time in ms before the checker aborts */
|
|
16
|
+
timeout_ms: number;
|
|
17
|
+
/** Whether to block the tool on failure (exit 2) or just warn */
|
|
18
|
+
block_on_failure: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** The fast gates that can run per-file in <200ms */
|
|
21
|
+
export declare const FAST_GATE_IDS: readonly ["hallucinated-imports", "promise-safety", "security-patterns", "file-size"];
|
|
22
|
+
export declare const DEFAULT_HOOK_CONFIG: HookConfig;
|
|
23
|
+
export type FastGateId = typeof FAST_GATE_IDS[number];
|
|
24
|
+
export interface HookCheckerResult {
|
|
25
|
+
status: 'pass' | 'fail' | 'error';
|
|
26
|
+
failures: Array<{
|
|
27
|
+
gate: string;
|
|
28
|
+
file: string;
|
|
29
|
+
message: string;
|
|
30
|
+
severity: string;
|
|
31
|
+
line?: number;
|
|
32
|
+
}>;
|
|
33
|
+
duration_ms: number;
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook system types for multi-tool integration.
|
|
3
|
+
*
|
|
4
|
+
* Each AI coding tool (Claude Code, Cursor, Cline, Windsurf)
|
|
5
|
+
* has its own hook format. These types unify the config generation.
|
|
6
|
+
*
|
|
7
|
+
* @since v3.0.0
|
|
8
|
+
*/
|
|
9
|
+
/** The fast gates that can run per-file in <200ms */
|
|
10
|
+
export const FAST_GATE_IDS = [
|
|
11
|
+
'hallucinated-imports',
|
|
12
|
+
'promise-safety',
|
|
13
|
+
'security-patterns',
|
|
14
|
+
'file-size',
|
|
15
|
+
];
|
|
16
|
+
export const DEFAULT_HOOK_CONFIG = {
|
|
17
|
+
tools: ['claude'],
|
|
18
|
+
fast_gates: [...FAST_GATE_IDS],
|
|
19
|
+
timeout_ms: 5000,
|
|
20
|
+
block_on_failure: false,
|
|
21
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -7,3 +7,5 @@ export * from './types/fix-packet.js';
|
|
|
7
7
|
export { Gate, GateContext } from './gates/base.js';
|
|
8
8
|
export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
|
|
9
9
|
export * from './utils/logger.js';
|
|
10
|
+
export * from './services/score-history.js';
|
|
11
|
+
export * from './hooks/index.js';
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,8 @@ export * from './types/fix-packet.js';
|
|
|
7
7
|
export { Gate } from './gates/base.js';
|
|
8
8
|
export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
|
|
9
9
|
export * from './utils/logger.js';
|
|
10
|
+
export * from './services/score-history.js';
|
|
11
|
+
export * from './hooks/index.js';
|
|
10
12
|
// Pattern Index is intentionally NOT exported here to prevent
|
|
11
13
|
// native dependency issues (sharp/transformers) from leaking into
|
|
12
14
|
// non-AI parts of the system.
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { FixPacketV2Schema } from '../types/fix-packet.js';
|
|
2
2
|
export class FixPacketService {
|
|
3
3
|
generate(report, config) {
|
|
4
|
-
|
|
4
|
+
// Sort violations: critical first, then high, medium, low, info
|
|
5
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
6
|
+
const violations = report.failures
|
|
7
|
+
.map(f => ({
|
|
5
8
|
id: f.id,
|
|
6
9
|
gate: f.id,
|
|
7
|
-
severity:
|
|
10
|
+
severity: (f.severity || 'medium'),
|
|
11
|
+
category: f.provenance,
|
|
8
12
|
title: f.title,
|
|
9
13
|
details: f.details,
|
|
10
14
|
files: f.files,
|
|
11
15
|
hint: f.hint,
|
|
12
|
-
instructions: f.hint ? [f.hint] : [],
|
|
16
|
+
instructions: f.hint ? [f.hint] : [],
|
|
13
17
|
metrics: f.metrics,
|
|
14
|
-
}))
|
|
18
|
+
}))
|
|
19
|
+
.sort((a, b) => (severityOrder[a.severity] ?? 2) - (severityOrder[b.severity] ?? 2));
|
|
15
20
|
const packet = {
|
|
16
21
|
version: 2,
|
|
17
22
|
goal: "Achieve PASS state by resolving all listed engineering violations.",
|
|
@@ -26,14 +31,4 @@ export class FixPacketService {
|
|
|
26
31
|
};
|
|
27
32
|
return FixPacketV2Schema.parse(packet);
|
|
28
33
|
}
|
|
29
|
-
inferSeverity(f) {
|
|
30
|
-
// High complexity or God objects are usually High severity
|
|
31
|
-
if (f.id === 'ast-analysis')
|
|
32
|
-
return 'high';
|
|
33
|
-
// Unit test or Lint failures are Medium
|
|
34
|
-
if (f.id === 'test' || f.id === 'lint')
|
|
35
|
-
return 'medium';
|
|
36
|
-
// Documentation or small file size issues are Low
|
|
37
|
-
return 'medium';
|
|
38
|
-
}
|
|
39
34
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Score History Service
|
|
3
|
+
*
|
|
4
|
+
* Append-only JSONL tracking of quality scores over time.
|
|
5
|
+
* Used for compliance dashboards, trend analysis, and audit reports.
|
|
6
|
+
*
|
|
7
|
+
* Uses JSONL (not JSON) to avoid read-modify-write race conditions
|
|
8
|
+
* when multiple agents run checks concurrently.
|
|
9
|
+
*
|
|
10
|
+
* @since v2.17.0
|
|
11
|
+
*/
|
|
12
|
+
export interface ScoreEntry {
|
|
13
|
+
timestamp: string;
|
|
14
|
+
status: 'PASS' | 'FAIL' | 'SKIP' | 'ERROR';
|
|
15
|
+
score: number;
|
|
16
|
+
ai_health_score?: number;
|
|
17
|
+
structural_score?: number;
|
|
18
|
+
failureCount: number;
|
|
19
|
+
severity_breakdown: Record<string, number>;
|
|
20
|
+
provenance_breakdown: Record<string, number>;
|
|
21
|
+
}
|
|
22
|
+
export interface ScoreTrend {
|
|
23
|
+
direction: 'improving' | 'stable' | 'degrading';
|
|
24
|
+
delta: number;
|
|
25
|
+
recentAvg: number;
|
|
26
|
+
previousAvg: number;
|
|
27
|
+
recentScores: number[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Record a score entry after a rigour check run.
|
|
31
|
+
* Appends a single JSONL line. Auto-trims to MAX_ENTRIES.
|
|
32
|
+
*/
|
|
33
|
+
export declare function recordScore(cwd: string, report: {
|
|
34
|
+
status: string;
|
|
35
|
+
stats: {
|
|
36
|
+
score?: number;
|
|
37
|
+
ai_health_score?: number;
|
|
38
|
+
structural_score?: number;
|
|
39
|
+
severity_breakdown?: Record<string, number>;
|
|
40
|
+
provenance_breakdown?: Record<string, number>;
|
|
41
|
+
};
|
|
42
|
+
failures: {
|
|
43
|
+
length: number;
|
|
44
|
+
} | any[];
|
|
45
|
+
}): void;
|
|
46
|
+
/**
|
|
47
|
+
* Read the last N score entries.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getScoreHistory(cwd: string, limit?: number): ScoreEntry[];
|
|
50
|
+
/**
|
|
51
|
+
* Calculate score trend from history.
|
|
52
|
+
* Compares average of last 5 runs vs previous 5 runs.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getScoreTrend(cwd: string): ScoreTrend | null;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Score History Service
|
|
3
|
+
*
|
|
4
|
+
* Append-only JSONL tracking of quality scores over time.
|
|
5
|
+
* Used for compliance dashboards, trend analysis, and audit reports.
|
|
6
|
+
*
|
|
7
|
+
* Uses JSONL (not JSON) to avoid read-modify-write race conditions
|
|
8
|
+
* when multiple agents run checks concurrently.
|
|
9
|
+
*
|
|
10
|
+
* @since v2.17.0
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
const MAX_ENTRIES = 100;
|
|
15
|
+
const HISTORY_FILE = 'score-history.jsonl';
|
|
16
|
+
function getHistoryPath(cwd) {
|
|
17
|
+
return path.join(cwd, '.rigour', HISTORY_FILE);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Record a score entry after a rigour check run.
|
|
21
|
+
* Appends a single JSONL line. Auto-trims to MAX_ENTRIES.
|
|
22
|
+
*/
|
|
23
|
+
export function recordScore(cwd, report) {
|
|
24
|
+
try {
|
|
25
|
+
const rigourDir = path.join(cwd, '.rigour');
|
|
26
|
+
if (!fs.existsSync(rigourDir)) {
|
|
27
|
+
fs.mkdirSync(rigourDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
const entry = {
|
|
30
|
+
timestamp: new Date().toISOString(),
|
|
31
|
+
status: report.status,
|
|
32
|
+
score: report.stats.score ?? 100,
|
|
33
|
+
ai_health_score: report.stats.ai_health_score,
|
|
34
|
+
structural_score: report.stats.structural_score,
|
|
35
|
+
failureCount: Array.isArray(report.failures) ? report.failures.length : 0,
|
|
36
|
+
severity_breakdown: report.stats.severity_breakdown ?? {},
|
|
37
|
+
provenance_breakdown: report.stats.provenance_breakdown ?? {},
|
|
38
|
+
};
|
|
39
|
+
const historyPath = getHistoryPath(cwd);
|
|
40
|
+
fs.appendFileSync(historyPath, JSON.stringify(entry) + '\n');
|
|
41
|
+
// Auto-trim if over MAX_ENTRIES
|
|
42
|
+
trimHistory(historyPath);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Silent fail — score tracking should never break the check command
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read the last N score entries.
|
|
50
|
+
*/
|
|
51
|
+
export function getScoreHistory(cwd, limit = 20) {
|
|
52
|
+
try {
|
|
53
|
+
const historyPath = getHistoryPath(cwd);
|
|
54
|
+
if (!fs.existsSync(historyPath))
|
|
55
|
+
return [];
|
|
56
|
+
const lines = fs.readFileSync(historyPath, 'utf-8')
|
|
57
|
+
.trim()
|
|
58
|
+
.split('\n')
|
|
59
|
+
.filter(line => line.length > 0);
|
|
60
|
+
const entries = lines.map(line => JSON.parse(line));
|
|
61
|
+
return entries.slice(-limit);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Calculate score trend from history.
|
|
69
|
+
* Compares average of last 5 runs vs previous 5 runs.
|
|
70
|
+
*/
|
|
71
|
+
export function getScoreTrend(cwd) {
|
|
72
|
+
const history = getScoreHistory(cwd, 20);
|
|
73
|
+
if (history.length < 3)
|
|
74
|
+
return null;
|
|
75
|
+
const scores = history.map(e => e.score);
|
|
76
|
+
const recentScores = scores.slice(-5);
|
|
77
|
+
const previousScores = scores.slice(-10, -5);
|
|
78
|
+
const recentAvg = recentScores.reduce((a, b) => a + b, 0) / recentScores.length;
|
|
79
|
+
if (previousScores.length === 0) {
|
|
80
|
+
return {
|
|
81
|
+
direction: 'stable',
|
|
82
|
+
delta: 0,
|
|
83
|
+
recentAvg: Math.round(recentAvg),
|
|
84
|
+
previousAvg: Math.round(recentAvg),
|
|
85
|
+
recentScores,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const previousAvg = previousScores.reduce((a, b) => a + b, 0) / previousScores.length;
|
|
89
|
+
const delta = recentAvg - previousAvg;
|
|
90
|
+
let direction;
|
|
91
|
+
if (delta > 3)
|
|
92
|
+
direction = 'improving';
|
|
93
|
+
else if (delta < -3)
|
|
94
|
+
direction = 'degrading';
|
|
95
|
+
else
|
|
96
|
+
direction = 'stable';
|
|
97
|
+
return {
|
|
98
|
+
direction,
|
|
99
|
+
delta: Math.round(delta * 10) / 10,
|
|
100
|
+
recentAvg: Math.round(recentAvg),
|
|
101
|
+
previousAvg: Math.round(previousAvg),
|
|
102
|
+
recentScores,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Trim JSONL file to last MAX_ENTRIES lines.
|
|
107
|
+
*/
|
|
108
|
+
function trimHistory(historyPath) {
|
|
109
|
+
try {
|
|
110
|
+
const lines = fs.readFileSync(historyPath, 'utf-8')
|
|
111
|
+
.trim()
|
|
112
|
+
.split('\n')
|
|
113
|
+
.filter(line => line.length > 0);
|
|
114
|
+
if (lines.length > MAX_ENTRIES) {
|
|
115
|
+
const trimmed = lines.slice(-MAX_ENTRIES);
|
|
116
|
+
fs.writeFileSync(historyPath, trimmed.join('\n') + '\n');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Silent fail
|
|
121
|
+
}
|
|
122
|
+
}
|