tlc-claude-code 1.6.4 → 1.8.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/dashboard/dist/components/ContainerSecurityPane.d.ts +45 -0
- package/dashboard/dist/components/ContainerSecurityPane.js +44 -0
- package/dashboard/dist/components/ContainerSecurityPane.test.d.ts +1 -0
- package/dashboard/dist/components/ContainerSecurityPane.test.js +153 -0
- package/package.json +1 -1
- package/server/lib/access-control.test.js +1 -1
- package/server/lib/agents-cancel-command.test.js +1 -1
- package/server/lib/agents-get-command.test.js +1 -1
- package/server/lib/agents-list-command.test.js +1 -1
- package/server/lib/agents-logs-command.test.js +1 -1
- package/server/lib/agents-retry-command.test.js +1 -1
- package/server/lib/budget-limits.test.js +2 -2
- package/server/lib/code-gate/bypass-logger.js +129 -0
- package/server/lib/code-gate/bypass-logger.test.js +142 -0
- package/server/lib/code-gate/first-commit-audit.js +138 -0
- package/server/lib/code-gate/first-commit-audit.test.js +203 -0
- package/server/lib/code-gate/gate-command.js +114 -0
- package/server/lib/code-gate/gate-command.test.js +111 -0
- package/server/lib/code-gate/gate-config.js +163 -0
- package/server/lib/code-gate/gate-config.test.js +181 -0
- package/server/lib/code-gate/gate-engine.js +193 -0
- package/server/lib/code-gate/gate-engine.test.js +258 -0
- package/server/lib/code-gate/gate-reporter.js +123 -0
- package/server/lib/code-gate/gate-reporter.test.js +159 -0
- package/server/lib/code-gate/hooks-generator.js +149 -0
- package/server/lib/code-gate/hooks-generator.test.js +142 -0
- package/server/lib/code-gate/llm-reviewer.js +176 -0
- package/server/lib/code-gate/llm-reviewer.test.js +161 -0
- package/server/lib/code-gate/multi-model-reviewer.js +172 -0
- package/server/lib/code-gate/multi-model-reviewer.test.js +217 -0
- package/server/lib/code-gate/push-gate.js +133 -0
- package/server/lib/code-gate/push-gate.test.js +190 -0
- package/server/lib/code-gate/rules/architecture-rules.js +228 -0
- package/server/lib/code-gate/rules/architecture-rules.test.js +155 -0
- package/server/lib/code-gate/rules/client-rules.js +120 -0
- package/server/lib/code-gate/rules/client-rules.test.js +121 -0
- package/server/lib/code-gate/rules/config-rules.js +140 -0
- package/server/lib/code-gate/rules/config-rules.test.js +103 -0
- package/server/lib/code-gate/rules/database-rules.js +158 -0
- package/server/lib/code-gate/rules/database-rules.test.js +119 -0
- package/server/lib/code-gate/rules/docker-rules.js +201 -0
- package/server/lib/code-gate/rules/docker-rules.test.js +104 -0
- package/server/lib/code-gate/rules/quality-rules.js +304 -0
- package/server/lib/code-gate/rules/quality-rules.test.js +199 -0
- package/server/lib/code-gate/rules/security-rules.js +228 -0
- package/server/lib/code-gate/rules/security-rules.test.js +131 -0
- package/server/lib/code-gate/rules/structure-rules.js +155 -0
- package/server/lib/code-gate/rules/structure-rules.test.js +107 -0
- package/server/lib/code-gate/rules/test-rules.js +93 -0
- package/server/lib/code-gate/rules/test-rules.test.js +97 -0
- package/server/lib/code-gate/typescript-gate.js +128 -0
- package/server/lib/code-gate/typescript-gate.test.js +131 -0
- package/server/lib/code-generator.test.js +1 -1
- package/server/lib/cost-command.test.js +1 -1
- package/server/lib/cost-optimizer.test.js +1 -1
- package/server/lib/cost-projections.test.js +1 -1
- package/server/lib/cost-reports.test.js +1 -1
- package/server/lib/cost-tracker.test.js +1 -1
- package/server/lib/crypto-patterns.test.js +1 -1
- package/server/lib/design-command.test.js +1 -1
- package/server/lib/design-parser.test.js +1 -1
- package/server/lib/gemini-vision.test.js +1 -1
- package/server/lib/infra/infra-generator.js +331 -0
- package/server/lib/infra/infra-generator.test.js +146 -0
- package/server/lib/input-validator.test.js +1 -1
- package/server/lib/litellm-client.test.js +1 -1
- package/server/lib/litellm-command.test.js +1 -1
- package/server/lib/litellm-config.test.js +1 -1
- package/server/lib/llm/adapters/api-adapter.js +95 -0
- package/server/lib/llm/adapters/api-adapter.test.js +81 -0
- package/server/lib/llm/adapters/codex-adapter.js +85 -0
- package/server/lib/llm/adapters/codex-adapter.test.js +54 -0
- package/server/lib/llm/adapters/gemini-adapter.js +100 -0
- package/server/lib/llm/adapters/gemini-adapter.test.js +54 -0
- package/server/lib/llm/index.js +109 -0
- package/server/lib/llm/index.test.js +147 -0
- package/server/lib/llm/provider-executor.js +168 -0
- package/server/lib/llm/provider-executor.test.js +244 -0
- package/server/lib/llm/provider-registry.js +104 -0
- package/server/lib/llm/provider-registry.test.js +157 -0
- package/server/lib/llm/review-service.js +222 -0
- package/server/lib/llm/review-service.test.js +220 -0
- package/server/lib/model-pricing.test.js +1 -1
- package/server/lib/models-command.test.js +1 -1
- package/server/lib/optimize-command.test.js +1 -1
- package/server/lib/orchestration-integration.test.js +1 -1
- package/server/lib/output-encoder.test.js +1 -1
- package/server/lib/quality-evaluator.test.js +1 -1
- package/server/lib/quality-gate-command.test.js +1 -1
- package/server/lib/quality-gate-scorer.test.js +1 -1
- package/server/lib/quality-history.test.js +1 -1
- package/server/lib/quality-presets.test.js +1 -1
- package/server/lib/quality-retry.test.js +1 -1
- package/server/lib/quality-thresholds.test.js +1 -1
- package/server/lib/secure-auth.test.js +1 -1
- package/server/lib/secure-code-command.test.js +1 -1
- package/server/lib/secure-errors.test.js +1 -1
- package/server/lib/security/auth-security.test.js +4 -3
- package/server/lib/shame/shame-registry.js +224 -0
- package/server/lib/shame/shame-registry.test.js +202 -0
- package/server/lib/standards/cleanup-dry-run.js +254 -0
- package/server/lib/standards/cleanup-dry-run.test.js +220 -0
- package/server/lib/vision-command.test.js +1 -1
- package/server/lib/visual-command.test.js +1 -1
- package/server/lib/visual-testing.test.js +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Configuration
|
|
3
|
+
*
|
|
4
|
+
* Reads gate config from .tlc.json, supports rule enable/disable,
|
|
5
|
+
* severity overrides, ignore patterns, and strictness levels.
|
|
6
|
+
*
|
|
7
|
+
* @module code-gate/gate-config
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
/** Strictness levels control default severity thresholds */
|
|
14
|
+
const STRICTNESS = {
|
|
15
|
+
RELAXED: 'relaxed',
|
|
16
|
+
STANDARD: 'standard',
|
|
17
|
+
STRICT: 'strict',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Return the default gate configuration.
|
|
22
|
+
* Defaults to strict mode — block on all high/critical findings.
|
|
23
|
+
*
|
|
24
|
+
* @returns {Object} Default gate config
|
|
25
|
+
*/
|
|
26
|
+
function getDefaultGateConfig() {
|
|
27
|
+
return {
|
|
28
|
+
enabled: true,
|
|
29
|
+
strictness: STRICTNESS.STRICT,
|
|
30
|
+
preCommit: true,
|
|
31
|
+
prePush: true,
|
|
32
|
+
rules: {},
|
|
33
|
+
ignore: ['*.md', '*.json', '*.lock', '*.yml', '*.yaml'],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load gate configuration from .tlc.json in the given project path.
|
|
39
|
+
* Falls back to defaults if no config exists or if parsing fails.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} projectPath - Path to project root
|
|
42
|
+
* @param {Object} [options] - Options with injectable dependencies
|
|
43
|
+
* @param {Object} [options.fs] - File system module (for testing)
|
|
44
|
+
* @returns {Object} Resolved gate configuration
|
|
45
|
+
*/
|
|
46
|
+
function loadGateConfig(projectPath, options = {}) {
|
|
47
|
+
const fsModule = options.fs || fs;
|
|
48
|
+
const defaults = getDefaultGateConfig();
|
|
49
|
+
const configPath = path.join(projectPath, '.tlc.json');
|
|
50
|
+
|
|
51
|
+
if (!fsModule.existsSync(configPath)) {
|
|
52
|
+
return defaults;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const raw = fsModule.readFileSync(configPath, 'utf-8');
|
|
57
|
+
const parsed = JSON.parse(raw);
|
|
58
|
+
const gateSection = parsed.gate || {};
|
|
59
|
+
return mergeGateConfig(defaults, gateSection);
|
|
60
|
+
} catch {
|
|
61
|
+
return defaults;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Deep-merge user gate config over defaults.
|
|
67
|
+
* Rules are merged as an override map. Ignore arrays are concatenated
|
|
68
|
+
* with deduplication.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} defaults - Default gate config
|
|
71
|
+
* @param {Object} userConfig - User overrides from .tlc.json
|
|
72
|
+
* @returns {Object} Merged config
|
|
73
|
+
*/
|
|
74
|
+
function mergeGateConfig(defaults, userConfig) {
|
|
75
|
+
const merged = { ...defaults };
|
|
76
|
+
|
|
77
|
+
// Simple scalar overrides
|
|
78
|
+
if (userConfig.enabled !== undefined) merged.enabled = userConfig.enabled;
|
|
79
|
+
if (userConfig.strictness !== undefined) merged.strictness = userConfig.strictness;
|
|
80
|
+
if (userConfig.preCommit !== undefined) merged.preCommit = userConfig.preCommit;
|
|
81
|
+
if (userConfig.prePush !== undefined) merged.prePush = userConfig.prePush;
|
|
82
|
+
|
|
83
|
+
// Merge rules as override map
|
|
84
|
+
if (userConfig.rules) {
|
|
85
|
+
merged.rules = { ...defaults.rules, ...userConfig.rules };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Concatenate ignore arrays, deduplicate
|
|
89
|
+
if (userConfig.ignore) {
|
|
90
|
+
const combined = [...defaults.ignore, ...userConfig.ignore];
|
|
91
|
+
merged.ignore = [...new Set(combined)];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return merged;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the effective severity for a rule, considering user overrides.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} ruleId - Rule identifier
|
|
101
|
+
* @param {string} defaultSeverity - The rule's built-in severity
|
|
102
|
+
* @param {Object} config - Gate config with rules overrides
|
|
103
|
+
* @returns {string|false} Effective severity, or false if rule is disabled
|
|
104
|
+
*/
|
|
105
|
+
function resolveRuleSeverity(ruleId, defaultSeverity, config) {
|
|
106
|
+
if (config.rules && config.rules[ruleId] !== undefined) {
|
|
107
|
+
return config.rules[ruleId];
|
|
108
|
+
}
|
|
109
|
+
return defaultSeverity;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if a file should be ignored based on config ignore patterns.
|
|
114
|
+
* Supports simple glob matching: *.ext and dir/* patterns.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} filePath - File path to check
|
|
117
|
+
* @param {Object} config - Gate config with ignore array
|
|
118
|
+
* @returns {boolean} True if file should be ignored
|
|
119
|
+
*/
|
|
120
|
+
function shouldIgnoreFile(filePath, config) {
|
|
121
|
+
const patterns = config.ignore || [];
|
|
122
|
+
for (const pattern of patterns) {
|
|
123
|
+
if (matchPattern(filePath, pattern)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Simple glob pattern matcher.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} filePath
|
|
134
|
+
* @param {string} pattern
|
|
135
|
+
* @returns {boolean}
|
|
136
|
+
*/
|
|
137
|
+
function matchPattern(filePath, pattern) {
|
|
138
|
+
// *.ext — match extension anywhere
|
|
139
|
+
if (pattern.startsWith('*.')) {
|
|
140
|
+
return filePath.endsWith(pattern.slice(1));
|
|
141
|
+
}
|
|
142
|
+
// **/*.ext — match extension in any nested path
|
|
143
|
+
if (pattern.startsWith('**/')) {
|
|
144
|
+
const sub = pattern.slice(3);
|
|
145
|
+
return matchPattern(filePath, sub) || matchPattern(path.basename(filePath), sub);
|
|
146
|
+
}
|
|
147
|
+
// dir/* — match files starting with dir/
|
|
148
|
+
if (pattern.endsWith('/*')) {
|
|
149
|
+
const dir = pattern.slice(0, -2);
|
|
150
|
+
return filePath.startsWith(dir + '/');
|
|
151
|
+
}
|
|
152
|
+
// Exact match
|
|
153
|
+
return filePath === pattern;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
loadGateConfig,
|
|
158
|
+
getDefaultGateConfig,
|
|
159
|
+
mergeGateConfig,
|
|
160
|
+
resolveRuleSeverity,
|
|
161
|
+
shouldIgnoreFile,
|
|
162
|
+
STRICTNESS,
|
|
163
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Configuration Tests
|
|
3
|
+
*
|
|
4
|
+
* Reads gate config from .tlc.json, supports rule enable/disable,
|
|
5
|
+
* severity overrides, ignore patterns, and strictness levels.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
loadGateConfig,
|
|
11
|
+
getDefaultGateConfig,
|
|
12
|
+
mergeGateConfig,
|
|
13
|
+
resolveRuleSeverity,
|
|
14
|
+
shouldIgnoreFile,
|
|
15
|
+
STRICTNESS,
|
|
16
|
+
} = require('./gate-config.js');
|
|
17
|
+
|
|
18
|
+
describe('Gate Configuration', () => {
|
|
19
|
+
describe('STRICTNESS', () => {
|
|
20
|
+
it('defines all strictness levels', () => {
|
|
21
|
+
expect(STRICTNESS.RELAXED).toBe('relaxed');
|
|
22
|
+
expect(STRICTNESS.STANDARD).toBe('standard');
|
|
23
|
+
expect(STRICTNESS.STRICT).toBe('strict');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('getDefaultGateConfig', () => {
|
|
28
|
+
it('returns default config with strict mode', () => {
|
|
29
|
+
const config = getDefaultGateConfig();
|
|
30
|
+
expect(config.enabled).toBe(true);
|
|
31
|
+
expect(config.strictness).toBe('strict');
|
|
32
|
+
expect(config.preCommit).toBe(true);
|
|
33
|
+
expect(config.prePush).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('includes default ignore patterns', () => {
|
|
37
|
+
const config = getDefaultGateConfig();
|
|
38
|
+
expect(config.ignore).toContain('*.md');
|
|
39
|
+
expect(config.ignore).toContain('*.json');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('has empty rules override by default', () => {
|
|
43
|
+
const config = getDefaultGateConfig();
|
|
44
|
+
expect(config.rules).toEqual({});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('loadGateConfig', () => {
|
|
49
|
+
it('returns defaults when no .tlc.json exists', () => {
|
|
50
|
+
const mockFs = {
|
|
51
|
+
existsSync: vi.fn().mockReturnValue(false),
|
|
52
|
+
};
|
|
53
|
+
const config = loadGateConfig('/project', { fs: mockFs });
|
|
54
|
+
expect(config.enabled).toBe(true);
|
|
55
|
+
expect(config.strictness).toBe('strict');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('reads gate section from .tlc.json', () => {
|
|
59
|
+
const tlcJson = {
|
|
60
|
+
gate: {
|
|
61
|
+
enabled: true,
|
|
62
|
+
strictness: 'standard',
|
|
63
|
+
rules: { 'no-hardcoded-urls': 'warn' },
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const mockFs = {
|
|
67
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
68
|
+
readFileSync: vi.fn().mockReturnValue(JSON.stringify(tlcJson)),
|
|
69
|
+
};
|
|
70
|
+
const config = loadGateConfig('/project', { fs: mockFs });
|
|
71
|
+
expect(config.strictness).toBe('standard');
|
|
72
|
+
expect(config.rules['no-hardcoded-urls']).toBe('warn');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('merges user config with defaults', () => {
|
|
76
|
+
const tlcJson = {
|
|
77
|
+
gate: {
|
|
78
|
+
strictness: 'relaxed',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const mockFs = {
|
|
82
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
83
|
+
readFileSync: vi.fn().mockReturnValue(JSON.stringify(tlcJson)),
|
|
84
|
+
};
|
|
85
|
+
const config = loadGateConfig('/project', { fs: mockFs });
|
|
86
|
+
// User override
|
|
87
|
+
expect(config.strictness).toBe('relaxed');
|
|
88
|
+
// Defaults preserved
|
|
89
|
+
expect(config.enabled).toBe(true);
|
|
90
|
+
expect(config.preCommit).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('handles malformed .tlc.json gracefully', () => {
|
|
94
|
+
const mockFs = {
|
|
95
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
96
|
+
readFileSync: vi.fn().mockReturnValue('not valid json{{{'),
|
|
97
|
+
};
|
|
98
|
+
const config = loadGateConfig('/project', { fs: mockFs });
|
|
99
|
+
expect(config.enabled).toBe(true);
|
|
100
|
+
expect(config.strictness).toBe('strict');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('handles missing gate section gracefully', () => {
|
|
104
|
+
const mockFs = {
|
|
105
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
106
|
+
readFileSync: vi.fn().mockReturnValue(JSON.stringify({ project: 'test' })),
|
|
107
|
+
};
|
|
108
|
+
const config = loadGateConfig('/project', { fs: mockFs });
|
|
109
|
+
expect(config.strictness).toBe('strict');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('mergeGateConfig', () => {
|
|
114
|
+
it('overrides defaults with user values', () => {
|
|
115
|
+
const defaults = getDefaultGateConfig();
|
|
116
|
+
const userConfig = { strictness: 'relaxed', prePush: false };
|
|
117
|
+
const merged = mergeGateConfig(defaults, userConfig);
|
|
118
|
+
expect(merged.strictness).toBe('relaxed');
|
|
119
|
+
expect(merged.prePush).toBe(false);
|
|
120
|
+
expect(merged.preCommit).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('merges rules as override map', () => {
|
|
124
|
+
const defaults = getDefaultGateConfig();
|
|
125
|
+
const userConfig = { rules: { 'no-eval': 'warn', 'no-hardcoded-urls': 'info' } };
|
|
126
|
+
const merged = mergeGateConfig(defaults, userConfig);
|
|
127
|
+
expect(merged.rules['no-eval']).toBe('warn');
|
|
128
|
+
expect(merged.rules['no-hardcoded-urls']).toBe('info');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('concatenates ignore arrays', () => {
|
|
132
|
+
const defaults = getDefaultGateConfig();
|
|
133
|
+
const userConfig = { ignore: ['migrations/*', 'generated/*'] };
|
|
134
|
+
const merged = mergeGateConfig(defaults, userConfig);
|
|
135
|
+
expect(merged.ignore).toContain('*.md');
|
|
136
|
+
expect(merged.ignore).toContain('migrations/*');
|
|
137
|
+
expect(merged.ignore).toContain('generated/*');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('resolveRuleSeverity', () => {
|
|
142
|
+
it('returns rule default when no override', () => {
|
|
143
|
+
const config = getDefaultGateConfig();
|
|
144
|
+
expect(resolveRuleSeverity('no-eval', 'block', config)).toBe('block');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('returns override when configured', () => {
|
|
148
|
+
const config = { ...getDefaultGateConfig(), rules: { 'no-eval': 'warn' } };
|
|
149
|
+
expect(resolveRuleSeverity('no-eval', 'block', config)).toBe('warn');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('disables rule when override is false', () => {
|
|
153
|
+
const config = { ...getDefaultGateConfig(), rules: { 'no-eval': false } };
|
|
154
|
+
expect(resolveRuleSeverity('no-eval', 'block', config)).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('shouldIgnoreFile', () => {
|
|
159
|
+
it('ignores files matching patterns', () => {
|
|
160
|
+
const config = { ignore: ['*.md', 'dist/*'] };
|
|
161
|
+
expect(shouldIgnoreFile('README.md', config)).toBe(true);
|
|
162
|
+
expect(shouldIgnoreFile('dist/bundle.js', config)).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('does not ignore non-matching files', () => {
|
|
166
|
+
const config = { ignore: ['*.md'] };
|
|
167
|
+
expect(shouldIgnoreFile('src/app.js', config)).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('handles empty ignore list', () => {
|
|
171
|
+
const config = { ignore: [] };
|
|
172
|
+
expect(shouldIgnoreFile('anything.js', config)).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('matches nested glob patterns', () => {
|
|
176
|
+
const config = { ignore: ['**/*.test.js'] };
|
|
177
|
+
expect(shouldIgnoreFile('src/deep/file.test.js', config)).toBe(true);
|
|
178
|
+
expect(shouldIgnoreFile('src/deep/file.js', config)).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Gate Engine
|
|
3
|
+
*
|
|
4
|
+
* Core engine that accepts changed files and runs configurable rule sets
|
|
5
|
+
* against each file, returning pass/fail with detailed findings per file.
|
|
6
|
+
*
|
|
7
|
+
* @module code-gate/gate-engine
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Severity levels for gate findings.
|
|
14
|
+
* - block: Commit/push is rejected
|
|
15
|
+
* - warn: Warning shown but allowed through
|
|
16
|
+
* - info: Informational, no action needed
|
|
17
|
+
*/
|
|
18
|
+
const SEVERITY = {
|
|
19
|
+
BLOCK: 'block',
|
|
20
|
+
WARN: 'warn',
|
|
21
|
+
INFO: 'info',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Deduction points per severity level for scoring */
|
|
25
|
+
const SCORE_DEDUCTIONS = {
|
|
26
|
+
[SEVERITY.BLOCK]: 25,
|
|
27
|
+
[SEVERITY.WARN]: 10,
|
|
28
|
+
[SEVERITY.INFO]: 2,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a gate engine instance with configurable rules and options.
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} options - Engine options
|
|
35
|
+
* @param {Array<{id: string, check: Function}>} [options.rules] - Rule set to run
|
|
36
|
+
* @param {string[]} [options.ignore] - Glob patterns for files to skip
|
|
37
|
+
* @returns {{ rules: Array, options: Object }}
|
|
38
|
+
*/
|
|
39
|
+
function createGateEngine(options = {}) {
|
|
40
|
+
return {
|
|
41
|
+
rules: options.rules || [],
|
|
42
|
+
options: {
|
|
43
|
+
ignore: options.ignore || [],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Run all configured rules against a set of changed files.
|
|
50
|
+
*
|
|
51
|
+
* @param {{ rules: Array, options: Object }} engine - Gate engine instance
|
|
52
|
+
* @param {Array<{path: string, content: string}>} files - Changed files to check
|
|
53
|
+
* @returns {Promise<{passed: boolean, findings: Array, summary: Object, duration: number}>}
|
|
54
|
+
*/
|
|
55
|
+
async function runGate(engine, files) {
|
|
56
|
+
const start = Date.now();
|
|
57
|
+
const findings = [];
|
|
58
|
+
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
if (shouldIgnore(file.path, engine.options.ignore)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const rule of engine.rules) {
|
|
65
|
+
try {
|
|
66
|
+
const ruleFindings = rule.check(file.path, file.content);
|
|
67
|
+
for (const finding of ruleFindings) {
|
|
68
|
+
findings.push({ ...finding, file: file.path });
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
findings.push({
|
|
72
|
+
severity: SEVERITY.WARN,
|
|
73
|
+
rule: rule.id,
|
|
74
|
+
file: file.path,
|
|
75
|
+
message: `Rule error: ${err.message}`,
|
|
76
|
+
fix: 'Check rule configuration',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const summary = buildSummary(findings);
|
|
83
|
+
const passed = summary.block === 0;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
passed,
|
|
87
|
+
findings,
|
|
88
|
+
summary,
|
|
89
|
+
duration: Date.now() - start,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Group findings by file path for reporting.
|
|
95
|
+
*
|
|
96
|
+
* @param {Array} findings - Array of finding objects
|
|
97
|
+
* @returns {Object.<string, Array>} Findings grouped by file
|
|
98
|
+
*/
|
|
99
|
+
function aggregateFindings(findings) {
|
|
100
|
+
const grouped = {};
|
|
101
|
+
for (const finding of findings) {
|
|
102
|
+
const key = finding.file;
|
|
103
|
+
if (!grouped[key]) {
|
|
104
|
+
grouped[key] = [];
|
|
105
|
+
}
|
|
106
|
+
grouped[key].push(finding);
|
|
107
|
+
}
|
|
108
|
+
return grouped;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Calculate a 0-100 quality score from findings.
|
|
113
|
+
* Starts at 100, deducts per severity. Floors at 0.
|
|
114
|
+
*
|
|
115
|
+
* @param {Array<{severity: string}>} findings - Finding objects with severity
|
|
116
|
+
* @returns {number} Score from 0 to 100
|
|
117
|
+
*/
|
|
118
|
+
function calculateScore(findings) {
|
|
119
|
+
let score = 100;
|
|
120
|
+
for (const finding of findings) {
|
|
121
|
+
const deduction = SCORE_DEDUCTIONS[finding.severity] || 0;
|
|
122
|
+
score -= deduction;
|
|
123
|
+
}
|
|
124
|
+
return Math.max(0, score);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build summary counts from findings array.
|
|
129
|
+
*
|
|
130
|
+
* @param {Array<{severity: string}>} findings
|
|
131
|
+
* @returns {{ total: number, block: number, warn: number, info: number }}
|
|
132
|
+
*/
|
|
133
|
+
function buildSummary(findings) {
|
|
134
|
+
const summary = { total: findings.length, block: 0, warn: 0, info: 0 };
|
|
135
|
+
for (const finding of findings) {
|
|
136
|
+
if (summary[finding.severity] !== undefined) {
|
|
137
|
+
summary[finding.severity]++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return summary;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if a file path matches any ignore pattern.
|
|
145
|
+
* Supports simple glob matching: *.ext and dir/* patterns.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} filePath - File path to check
|
|
148
|
+
* @param {string[]} patterns - Glob patterns
|
|
149
|
+
* @returns {boolean} True if file should be ignored
|
|
150
|
+
*/
|
|
151
|
+
function shouldIgnore(filePath, patterns) {
|
|
152
|
+
for (const pattern of patterns) {
|
|
153
|
+
if (matchGlob(filePath, pattern)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Simple glob matcher for common patterns.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} filePath
|
|
164
|
+
* @param {string} pattern
|
|
165
|
+
* @returns {boolean}
|
|
166
|
+
*/
|
|
167
|
+
function matchGlob(filePath, pattern) {
|
|
168
|
+
// *.ext — match extension anywhere
|
|
169
|
+
if (pattern.startsWith('*.')) {
|
|
170
|
+
const ext = pattern.slice(1);
|
|
171
|
+
return filePath.endsWith(ext);
|
|
172
|
+
}
|
|
173
|
+
// **/*.ext — match extension in any directory
|
|
174
|
+
if (pattern.startsWith('**/')) {
|
|
175
|
+
const sub = pattern.slice(3);
|
|
176
|
+
return matchGlob(filePath, sub) || matchGlob(path.basename(filePath), sub);
|
|
177
|
+
}
|
|
178
|
+
// dir/* — match files in directory
|
|
179
|
+
if (pattern.endsWith('/*')) {
|
|
180
|
+
const dir = pattern.slice(0, -2);
|
|
181
|
+
return filePath.startsWith(dir + '/') || filePath.startsWith(dir + path.sep);
|
|
182
|
+
}
|
|
183
|
+
// Exact match
|
|
184
|
+
return filePath === pattern;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
createGateEngine,
|
|
189
|
+
runGate,
|
|
190
|
+
aggregateFindings,
|
|
191
|
+
calculateScore,
|
|
192
|
+
SEVERITY,
|
|
193
|
+
};
|