@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.
Files changed (117) hide show
  1. package/README.md +58 -0
  2. package/dist/context.test.js +2 -3
  3. package/dist/environment.test.js +2 -1
  4. package/dist/gates/agent-team.d.ts +2 -1
  5. package/dist/gates/agent-team.js +1 -0
  6. package/dist/gates/base.d.ts +3 -1
  7. package/dist/gates/base.js +3 -0
  8. package/dist/gates/checkpoint.d.ts +2 -1
  9. package/dist/gates/checkpoint.js +3 -2
  10. package/dist/gates/context-window-artifacts.d.ts +2 -1
  11. package/dist/gates/context-window-artifacts.js +6 -3
  12. package/dist/gates/context.d.ts +2 -1
  13. package/dist/gates/context.js +1 -0
  14. package/dist/gates/coverage.js +3 -1
  15. package/dist/gates/dependency.js +5 -5
  16. package/dist/gates/duplication-drift.d.ts +2 -1
  17. package/dist/gates/duplication-drift.js +4 -1
  18. package/dist/gates/environment.js +4 -4
  19. package/dist/gates/hallucinated-imports.d.ts +21 -2
  20. package/dist/gates/hallucinated-imports.js +116 -2
  21. package/dist/gates/inconsistent-error-handling.d.ts +2 -1
  22. package/dist/gates/inconsistent-error-handling.js +21 -7
  23. package/dist/gates/promise-safety.d.ts +68 -0
  24. package/dist/gates/promise-safety.js +509 -0
  25. package/dist/gates/retry-loop-breaker.d.ts +2 -1
  26. package/dist/gates/retry-loop-breaker.js +2 -1
  27. package/dist/gates/runner.js +34 -1
  28. package/dist/gates/safety.d.ts +2 -1
  29. package/dist/gates/safety.js +2 -1
  30. package/dist/gates/security-patterns-owasp.test.d.ts +1 -0
  31. package/dist/gates/security-patterns-owasp.test.js +171 -0
  32. package/dist/gates/security-patterns.d.ts +6 -1
  33. package/dist/gates/security-patterns.js +101 -0
  34. package/dist/gates/structure.js +1 -1
  35. package/dist/hooks/checker.d.ts +23 -0
  36. package/dist/hooks/checker.js +222 -0
  37. package/dist/hooks/checker.test.d.ts +1 -0
  38. package/dist/hooks/checker.test.js +132 -0
  39. package/dist/hooks/index.d.ts +9 -0
  40. package/dist/hooks/index.js +8 -0
  41. package/dist/hooks/standalone-checker.d.ts +15 -0
  42. package/dist/hooks/standalone-checker.js +106 -0
  43. package/dist/hooks/templates.d.ts +22 -0
  44. package/dist/hooks/templates.js +232 -0
  45. package/dist/hooks/types.d.ts +34 -0
  46. package/dist/hooks/types.js +21 -0
  47. package/dist/index.d.ts +2 -0
  48. package/dist/index.js +2 -0
  49. package/dist/services/fix-packet-service.d.ts +0 -1
  50. package/dist/services/fix-packet-service.js +9 -14
  51. package/dist/services/score-history.d.ts +54 -0
  52. package/dist/services/score-history.js +122 -0
  53. package/dist/templates/index.js +176 -0
  54. package/dist/types/fix-packet.d.ts +5 -5
  55. package/dist/types/fix-packet.js +1 -1
  56. package/dist/types/index.d.ts +207 -0
  57. package/dist/types/index.js +32 -0
  58. package/package.json +21 -1
  59. package/src/context.test.ts +0 -256
  60. package/src/discovery.test.ts +0 -88
  61. package/src/discovery.ts +0 -112
  62. package/src/environment.test.ts +0 -115
  63. package/src/gates/agent-team.test.ts +0 -134
  64. package/src/gates/agent-team.ts +0 -210
  65. package/src/gates/ast-handlers/base.ts +0 -13
  66. package/src/gates/ast-handlers/python.ts +0 -145
  67. package/src/gates/ast-handlers/python_parser.py +0 -181
  68. package/src/gates/ast-handlers/typescript.ts +0 -264
  69. package/src/gates/ast-handlers/universal.ts +0 -184
  70. package/src/gates/ast.ts +0 -54
  71. package/src/gates/base.ts +0 -28
  72. package/src/gates/checkpoint.test.ts +0 -135
  73. package/src/gates/checkpoint.ts +0 -311
  74. package/src/gates/content.ts +0 -51
  75. package/src/gates/context-window-artifacts.ts +0 -277
  76. package/src/gates/context.ts +0 -270
  77. package/src/gates/coverage.ts +0 -74
  78. package/src/gates/dependency.ts +0 -108
  79. package/src/gates/duplication-drift.ts +0 -231
  80. package/src/gates/environment.ts +0 -94
  81. package/src/gates/file.ts +0 -46
  82. package/src/gates/hallucinated-imports.ts +0 -361
  83. package/src/gates/inconsistent-error-handling.ts +0 -254
  84. package/src/gates/retry-loop-breaker.ts +0 -151
  85. package/src/gates/runner.ts +0 -188
  86. package/src/gates/safety.ts +0 -56
  87. package/src/gates/security-patterns.test.ts +0 -162
  88. package/src/gates/security-patterns.ts +0 -306
  89. package/src/gates/structure.ts +0 -36
  90. package/src/index.ts +0 -13
  91. package/src/pattern-index/embeddings.ts +0 -84
  92. package/src/pattern-index/index.ts +0 -59
  93. package/src/pattern-index/indexer.test.ts +0 -276
  94. package/src/pattern-index/indexer.ts +0 -1023
  95. package/src/pattern-index/matcher.test.ts +0 -293
  96. package/src/pattern-index/matcher.ts +0 -493
  97. package/src/pattern-index/overrides.ts +0 -235
  98. package/src/pattern-index/security.ts +0 -151
  99. package/src/pattern-index/staleness.test.ts +0 -313
  100. package/src/pattern-index/staleness.ts +0 -568
  101. package/src/pattern-index/types.ts +0 -339
  102. package/src/safety.test.ts +0 -53
  103. package/src/services/adaptive-thresholds.test.ts +0 -189
  104. package/src/services/adaptive-thresholds.ts +0 -275
  105. package/src/services/context-engine.ts +0 -104
  106. package/src/services/fix-packet-service.ts +0 -42
  107. package/src/services/state-service.ts +0 -138
  108. package/src/smoke.test.ts +0 -18
  109. package/src/templates/index.ts +0 -338
  110. package/src/types/fix-packet.ts +0 -32
  111. package/src/types/index.ts +0 -200
  112. package/src/utils/logger.ts +0 -43
  113. package/src/utils/scanner.test.ts +0 -37
  114. package/src/utils/scanner.ts +0 -43
  115. package/tsconfig.json +0 -10
  116. package/vitest.config.ts +0 -7
  117. 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.
@@ -2,5 +2,4 @@ import { Report, Config } from '../types/index.js';
2
2
  import { FixPacketV2 } from '../types/fix-packet.js';
3
3
  export declare class FixPacketService {
4
4
  generate(report: Report, config: Config): FixPacketV2;
5
- private inferSeverity;
6
5
  }
@@ -1,17 +1,22 @@
1
1
  import { FixPacketV2Schema } from '../types/fix-packet.js';
2
2
  export class FixPacketService {
3
3
  generate(report, config) {
4
- const violations = report.failures.map(f => ({
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: this.inferSeverity(f),
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] : [], // Use hint as first instruction
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
+ }