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.
Files changed (105) hide show
  1. package/dashboard/dist/components/ContainerSecurityPane.d.ts +45 -0
  2. package/dashboard/dist/components/ContainerSecurityPane.js +44 -0
  3. package/dashboard/dist/components/ContainerSecurityPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/ContainerSecurityPane.test.js +153 -0
  5. package/package.json +1 -1
  6. package/server/lib/access-control.test.js +1 -1
  7. package/server/lib/agents-cancel-command.test.js +1 -1
  8. package/server/lib/agents-get-command.test.js +1 -1
  9. package/server/lib/agents-list-command.test.js +1 -1
  10. package/server/lib/agents-logs-command.test.js +1 -1
  11. package/server/lib/agents-retry-command.test.js +1 -1
  12. package/server/lib/budget-limits.test.js +2 -2
  13. package/server/lib/code-gate/bypass-logger.js +129 -0
  14. package/server/lib/code-gate/bypass-logger.test.js +142 -0
  15. package/server/lib/code-gate/first-commit-audit.js +138 -0
  16. package/server/lib/code-gate/first-commit-audit.test.js +203 -0
  17. package/server/lib/code-gate/gate-command.js +114 -0
  18. package/server/lib/code-gate/gate-command.test.js +111 -0
  19. package/server/lib/code-gate/gate-config.js +163 -0
  20. package/server/lib/code-gate/gate-config.test.js +181 -0
  21. package/server/lib/code-gate/gate-engine.js +193 -0
  22. package/server/lib/code-gate/gate-engine.test.js +258 -0
  23. package/server/lib/code-gate/gate-reporter.js +123 -0
  24. package/server/lib/code-gate/gate-reporter.test.js +159 -0
  25. package/server/lib/code-gate/hooks-generator.js +149 -0
  26. package/server/lib/code-gate/hooks-generator.test.js +142 -0
  27. package/server/lib/code-gate/llm-reviewer.js +176 -0
  28. package/server/lib/code-gate/llm-reviewer.test.js +161 -0
  29. package/server/lib/code-gate/multi-model-reviewer.js +172 -0
  30. package/server/lib/code-gate/multi-model-reviewer.test.js +217 -0
  31. package/server/lib/code-gate/push-gate.js +133 -0
  32. package/server/lib/code-gate/push-gate.test.js +190 -0
  33. package/server/lib/code-gate/rules/architecture-rules.js +228 -0
  34. package/server/lib/code-gate/rules/architecture-rules.test.js +155 -0
  35. package/server/lib/code-gate/rules/client-rules.js +120 -0
  36. package/server/lib/code-gate/rules/client-rules.test.js +121 -0
  37. package/server/lib/code-gate/rules/config-rules.js +140 -0
  38. package/server/lib/code-gate/rules/config-rules.test.js +103 -0
  39. package/server/lib/code-gate/rules/database-rules.js +158 -0
  40. package/server/lib/code-gate/rules/database-rules.test.js +119 -0
  41. package/server/lib/code-gate/rules/docker-rules.js +201 -0
  42. package/server/lib/code-gate/rules/docker-rules.test.js +104 -0
  43. package/server/lib/code-gate/rules/quality-rules.js +304 -0
  44. package/server/lib/code-gate/rules/quality-rules.test.js +199 -0
  45. package/server/lib/code-gate/rules/security-rules.js +228 -0
  46. package/server/lib/code-gate/rules/security-rules.test.js +131 -0
  47. package/server/lib/code-gate/rules/structure-rules.js +155 -0
  48. package/server/lib/code-gate/rules/structure-rules.test.js +107 -0
  49. package/server/lib/code-gate/rules/test-rules.js +93 -0
  50. package/server/lib/code-gate/rules/test-rules.test.js +97 -0
  51. package/server/lib/code-gate/typescript-gate.js +128 -0
  52. package/server/lib/code-gate/typescript-gate.test.js +131 -0
  53. package/server/lib/code-generator.test.js +1 -1
  54. package/server/lib/cost-command.test.js +1 -1
  55. package/server/lib/cost-optimizer.test.js +1 -1
  56. package/server/lib/cost-projections.test.js +1 -1
  57. package/server/lib/cost-reports.test.js +1 -1
  58. package/server/lib/cost-tracker.test.js +1 -1
  59. package/server/lib/crypto-patterns.test.js +1 -1
  60. package/server/lib/design-command.test.js +1 -1
  61. package/server/lib/design-parser.test.js +1 -1
  62. package/server/lib/gemini-vision.test.js +1 -1
  63. package/server/lib/infra/infra-generator.js +331 -0
  64. package/server/lib/infra/infra-generator.test.js +146 -0
  65. package/server/lib/input-validator.test.js +1 -1
  66. package/server/lib/litellm-client.test.js +1 -1
  67. package/server/lib/litellm-command.test.js +1 -1
  68. package/server/lib/litellm-config.test.js +1 -1
  69. package/server/lib/llm/adapters/api-adapter.js +95 -0
  70. package/server/lib/llm/adapters/api-adapter.test.js +81 -0
  71. package/server/lib/llm/adapters/codex-adapter.js +85 -0
  72. package/server/lib/llm/adapters/codex-adapter.test.js +54 -0
  73. package/server/lib/llm/adapters/gemini-adapter.js +100 -0
  74. package/server/lib/llm/adapters/gemini-adapter.test.js +54 -0
  75. package/server/lib/llm/index.js +109 -0
  76. package/server/lib/llm/index.test.js +147 -0
  77. package/server/lib/llm/provider-executor.js +168 -0
  78. package/server/lib/llm/provider-executor.test.js +244 -0
  79. package/server/lib/llm/provider-registry.js +104 -0
  80. package/server/lib/llm/provider-registry.test.js +157 -0
  81. package/server/lib/llm/review-service.js +222 -0
  82. package/server/lib/llm/review-service.test.js +220 -0
  83. package/server/lib/model-pricing.test.js +1 -1
  84. package/server/lib/models-command.test.js +1 -1
  85. package/server/lib/optimize-command.test.js +1 -1
  86. package/server/lib/orchestration-integration.test.js +1 -1
  87. package/server/lib/output-encoder.test.js +1 -1
  88. package/server/lib/quality-evaluator.test.js +1 -1
  89. package/server/lib/quality-gate-command.test.js +1 -1
  90. package/server/lib/quality-gate-scorer.test.js +1 -1
  91. package/server/lib/quality-history.test.js +1 -1
  92. package/server/lib/quality-presets.test.js +1 -1
  93. package/server/lib/quality-retry.test.js +1 -1
  94. package/server/lib/quality-thresholds.test.js +1 -1
  95. package/server/lib/secure-auth.test.js +1 -1
  96. package/server/lib/secure-code-command.test.js +1 -1
  97. package/server/lib/secure-errors.test.js +1 -1
  98. package/server/lib/security/auth-security.test.js +4 -3
  99. package/server/lib/shame/shame-registry.js +224 -0
  100. package/server/lib/shame/shame-registry.test.js +202 -0
  101. package/server/lib/standards/cleanup-dry-run.js +254 -0
  102. package/server/lib/standards/cleanup-dry-run.test.js +220 -0
  103. package/server/lib/vision-command.test.js +1 -1
  104. package/server/lib/visual-command.test.js +1 -1
  105. 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
+ };