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,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Rules
|
|
3
|
+
*
|
|
4
|
+
* Detects magic numbers (hardcoded timeouts, durations, thresholds)
|
|
5
|
+
* and hardcoded role strings that should live in config or database.
|
|
6
|
+
*
|
|
7
|
+
* Derived from Bug #18 (hardcoded role mappings) and Bug #23 (magic numbers).
|
|
8
|
+
*
|
|
9
|
+
* @module code-gate/rules/config-rules
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} filePath
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
function isTestFile(filePath) {
|
|
17
|
+
return /\.(test|spec)\.[jt]sx?$/.test(filePath) || filePath.includes('__tests__');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} filePath
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
function isConfigFile(filePath) {
|
|
25
|
+
const base = filePath.toLowerCase();
|
|
26
|
+
return base.includes('config') || base.includes('constants') ||
|
|
27
|
+
base.includes('.env') || base.endsWith('.json') ||
|
|
28
|
+
base.endsWith('.yml') || base.endsWith('.yaml');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Numbers that are safe to hardcode (HTTP status codes, common values) */
|
|
32
|
+
const SAFE_NUMBERS = new Set([
|
|
33
|
+
0, 1, 2, 3, 4, 5, 10, 24, 60, 100,
|
|
34
|
+
200, 201, 204, 301, 302, 304, 400, 401, 403, 404, 409, 422, 429, 500, 502, 503,
|
|
35
|
+
1000, // 1 second in ms
|
|
36
|
+
1024, 2048, 4096, // byte sizes
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/** Threshold above which a number is suspicious (1 minute in ms) */
|
|
40
|
+
const MAGIC_THRESHOLD = 60000;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detect large hardcoded numbers that look like timeouts or durations.
|
|
44
|
+
* Numbers >= 60000 in assignments (not in const UPPER_CASE declarations)
|
|
45
|
+
* are flagged as likely magic numbers.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} filePath
|
|
48
|
+
* @param {string} content
|
|
49
|
+
* @returns {Array<{severity: string, rule: string, line: number, message: string, fix: string}>}
|
|
50
|
+
*/
|
|
51
|
+
function checkMagicNumbers(filePath, content) {
|
|
52
|
+
if (isTestFile(filePath)) return [];
|
|
53
|
+
if (isConfigFile(filePath)) return [];
|
|
54
|
+
const findings = [];
|
|
55
|
+
const lines = content.split('\n');
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
const line = lines[i];
|
|
59
|
+
const trimmed = line.trim();
|
|
60
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
|
|
61
|
+
|
|
62
|
+
// Find number literals >= MAGIC_THRESHOLD
|
|
63
|
+
const numberMatches = line.matchAll(/\b(\d{5,})\b/g);
|
|
64
|
+
for (const match of numberMatches) {
|
|
65
|
+
const num = parseInt(match[1], 10);
|
|
66
|
+
if (num < MAGIC_THRESHOLD || SAFE_NUMBERS.has(num)) continue;
|
|
67
|
+
|
|
68
|
+
// Allow if it's a UPPER_CASE constant declaration
|
|
69
|
+
if (/\b(?:const|let|var)\s+[A-Z][A-Z0-9_]+\s*=/.test(line)) continue;
|
|
70
|
+
|
|
71
|
+
findings.push({
|
|
72
|
+
severity: 'warn',
|
|
73
|
+
rule: 'no-magic-numbers',
|
|
74
|
+
line: i + 1,
|
|
75
|
+
message: `Magic number ${num} — use a named constant or config value`,
|
|
76
|
+
fix: 'Extract to a named constant (e.g. const SESSION_TIMEOUT = ...) or use config',
|
|
77
|
+
});
|
|
78
|
+
break; // One finding per line
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return findings;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Role-related keywords to detect in comparisons and object literals */
|
|
86
|
+
const ROLE_STRINGS = [
|
|
87
|
+
'admin', 'manager', 'editor', 'viewer', 'user', 'moderator',
|
|
88
|
+
'owner', 'member', 'guest', 'superadmin', 'super_admin',
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Detect hardcoded role strings in comparisons and object literals.
|
|
93
|
+
* Roles should come from constants, RBAC tables, or config — not inline strings.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} filePath
|
|
96
|
+
* @param {string} content
|
|
97
|
+
* @returns {Array}
|
|
98
|
+
*/
|
|
99
|
+
function checkHardcodedRoles(filePath, content) {
|
|
100
|
+
if (isTestFile(filePath)) return [];
|
|
101
|
+
|
|
102
|
+
// Skip role definition files
|
|
103
|
+
const base = filePath.toLowerCase();
|
|
104
|
+
if (base.includes('role') || base.includes('permission') ||
|
|
105
|
+
base.includes('rbac') || isConfigFile(filePath)) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const findings = [];
|
|
110
|
+
const lines = content.split('\n');
|
|
111
|
+
const rolePattern = new RegExp(
|
|
112
|
+
`(?:role|userRole|workspaceRole)\\s*(?:===?|!==?)\\s*['"\`](${ROLE_STRINGS.join('|')})['"\`]`
|
|
113
|
+
);
|
|
114
|
+
const roleLiteralPattern = new RegExp(
|
|
115
|
+
`\\brole\\s*:\\s*['"\`](${ROLE_STRINGS.join('|')})['"\`]`
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < lines.length; i++) {
|
|
119
|
+
const line = lines[i];
|
|
120
|
+
const trimmed = line.trim();
|
|
121
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
|
|
122
|
+
|
|
123
|
+
if (rolePattern.test(line) || roleLiteralPattern.test(line)) {
|
|
124
|
+
findings.push({
|
|
125
|
+
severity: 'warn',
|
|
126
|
+
rule: 'no-hardcoded-roles',
|
|
127
|
+
line: i + 1,
|
|
128
|
+
message: 'Hardcoded role string — use RBAC constants or database roles',
|
|
129
|
+
fix: 'Import role constants (e.g. ROLES.ADMIN) instead of string literals',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return findings;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
checkMagicNumbers,
|
|
139
|
+
checkHardcodedRoles,
|
|
140
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Rules Tests
|
|
3
|
+
*
|
|
4
|
+
* Detects magic numbers (hardcoded timeouts/thresholds)
|
|
5
|
+
* and hardcoded role strings that should be in config/DB.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
checkMagicNumbers,
|
|
11
|
+
checkHardcodedRoles,
|
|
12
|
+
} = require('./config-rules.js');
|
|
13
|
+
|
|
14
|
+
describe('Config Rules', () => {
|
|
15
|
+
describe('checkMagicNumbers', () => {
|
|
16
|
+
it('detects hardcoded 86400000 (24h in ms)', () => {
|
|
17
|
+
const code = 'const sessionTimeout = 86400000;';
|
|
18
|
+
const findings = checkMagicNumbers('src/auth/session.ts', code);
|
|
19
|
+
expect(findings).toHaveLength(1);
|
|
20
|
+
expect(findings[0].severity).toBe('warn');
|
|
21
|
+
expect(findings[0].rule).toBe('no-magic-numbers');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('detects hardcoded 3600000 (1h in ms)', () => {
|
|
25
|
+
const code = 'const resetExpiry = 3600000;';
|
|
26
|
+
const findings = checkMagicNumbers('src/auth/reset.ts', code);
|
|
27
|
+
expect(findings).toHaveLength(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('allows named constants (const TIMEOUT = 86400000)', () => {
|
|
31
|
+
// Already a named constant at declaration — this is the pattern we want
|
|
32
|
+
const code = 'const SESSION_TIMEOUT = 86400000;';
|
|
33
|
+
const findings = checkMagicNumbers('src/config/constants.ts', code);
|
|
34
|
+
expect(findings).toHaveLength(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('allows common safe numbers', () => {
|
|
38
|
+
const code = [
|
|
39
|
+
'const count = 0;',
|
|
40
|
+
'const index = 1;',
|
|
41
|
+
'const percent = 100;',
|
|
42
|
+
'return res.status(200).json(data);',
|
|
43
|
+
'return res.status(404).json({ error: "Not found" });',
|
|
44
|
+
'return res.status(500).json({ error: "Server error" });',
|
|
45
|
+
].join('\n');
|
|
46
|
+
const findings = checkMagicNumbers('src/api/handler.ts', code);
|
|
47
|
+
expect(findings).toHaveLength(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('skips test files', () => {
|
|
51
|
+
const code = 'const timeout = 86400000;';
|
|
52
|
+
const findings = checkMagicNumbers('src/auth/session.test.ts', code);
|
|
53
|
+
expect(findings).toHaveLength(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('skips config and constants files', () => {
|
|
57
|
+
const code = 'const timeout = 86400000;';
|
|
58
|
+
const findings = checkMagicNumbers('src/config/timeouts.ts', code);
|
|
59
|
+
expect(findings).toHaveLength(0);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('checkHardcodedRoles', () => {
|
|
64
|
+
it('detects role === "admin" comparison', () => {
|
|
65
|
+
const code = 'if (user.role === "admin") { allowAccess(); }';
|
|
66
|
+
const findings = checkHardcodedRoles('src/middleware/auth.ts', code);
|
|
67
|
+
expect(findings).toHaveLength(1);
|
|
68
|
+
expect(findings[0].severity).toBe('warn');
|
|
69
|
+
expect(findings[0].rule).toBe('no-hardcoded-roles');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('detects role: "manager" in object literal', () => {
|
|
73
|
+
const code = 'const defaultUser = { name: "test", role: "manager" };';
|
|
74
|
+
const findings = checkHardcodedRoles('src/api/users.ts', code);
|
|
75
|
+
expect(findings).toHaveLength(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('allows role variable references without string', () => {
|
|
79
|
+
const code = 'if (user.role === ROLES.ADMIN) { allowAccess(); }';
|
|
80
|
+
const findings = checkHardcodedRoles('src/middleware/auth.ts', code);
|
|
81
|
+
expect(findings).toHaveLength(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('allows RBAC constant definitions', () => {
|
|
85
|
+
// This is WHERE roles are defined — that's fine
|
|
86
|
+
const code = 'const ROLES = { ADMIN: "admin", USER: "user" };';
|
|
87
|
+
const findings = checkHardcodedRoles('src/config/roles.ts', code);
|
|
88
|
+
expect(findings).toHaveLength(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('skips test files', () => {
|
|
92
|
+
const code = 'if (user.role === "admin") {}';
|
|
93
|
+
const findings = checkHardcodedRoles('src/middleware/auth.test.ts', code);
|
|
94
|
+
expect(findings).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('skips role definition files', () => {
|
|
98
|
+
const code = 'role: "admin"';
|
|
99
|
+
const findings = checkHardcodedRoles('src/permissions/roles.ts', code);
|
|
100
|
+
expect(findings).toHaveLength(0);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Rules
|
|
3
|
+
*
|
|
4
|
+
* Detects new Date() in ORM .set() blocks (should use sql`now()`)
|
|
5
|
+
* and inline billing math that should use shared calculation utilities.
|
|
6
|
+
*
|
|
7
|
+
* Derived from production bugs: timestamp drift, copy-paste billing errors.
|
|
8
|
+
* See: WALL_OF_SHAME.md Bug #29 (missing VAT), Lesson #3 (timestamps)
|
|
9
|
+
*
|
|
10
|
+
* @module code-gate/rules/database-rules
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} filePath
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
function isTestFile(filePath) {
|
|
18
|
+
return /\.(test|spec)\.[jt]sx?$/.test(filePath) || filePath.includes('__tests__');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detect new Date() inside ORM .set({}) blocks.
|
|
23
|
+
* Database timestamps should use sql`now()` for consistent DB-server time.
|
|
24
|
+
* new Date() uses app-server time which can drift.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} filePath
|
|
27
|
+
* @param {string} content
|
|
28
|
+
* @returns {Array<{severity: string, rule: string, line: number, message: string, fix: string}>}
|
|
29
|
+
*/
|
|
30
|
+
function checkNewDateInSet(filePath, content) {
|
|
31
|
+
if (isTestFile(filePath)) return [];
|
|
32
|
+
const findings = [];
|
|
33
|
+
const lines = content.split('\n');
|
|
34
|
+
|
|
35
|
+
let inSetBlock = false;
|
|
36
|
+
let braceDepth = 0;
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < lines.length; i++) {
|
|
39
|
+
const line = lines[i];
|
|
40
|
+
|
|
41
|
+
// Detect .set({ or .set( { start
|
|
42
|
+
if (/\.set\s*\(\s*\{/.test(line)) {
|
|
43
|
+
inSetBlock = true;
|
|
44
|
+
braceDepth = 0;
|
|
45
|
+
// Count braces on this line
|
|
46
|
+
for (const ch of line) {
|
|
47
|
+
if (ch === '{') braceDepth++;
|
|
48
|
+
if (ch === '}') braceDepth--;
|
|
49
|
+
}
|
|
50
|
+
if (braceDepth <= 0) {
|
|
51
|
+
// Single-line .set({...}) — check this line
|
|
52
|
+
if (/new\s+Date\s*\(\s*\)/.test(line)) {
|
|
53
|
+
findings.push({
|
|
54
|
+
severity: 'block',
|
|
55
|
+
rule: 'no-new-date-in-set',
|
|
56
|
+
line: i + 1,
|
|
57
|
+
message: 'new Date() in .set() block — use sql`now()` for consistent DB timestamps',
|
|
58
|
+
fix: 'Replace new Date() with sql`now()` for database-server time consistency',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
inSetBlock = false;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Check the opening line too
|
|
65
|
+
if (/new\s+Date\s*\(\s*\)/.test(line)) {
|
|
66
|
+
findings.push({
|
|
67
|
+
severity: 'block',
|
|
68
|
+
rule: 'no-new-date-in-set',
|
|
69
|
+
line: i + 1,
|
|
70
|
+
message: 'new Date() in .set() block — use sql`now()` for consistent DB timestamps',
|
|
71
|
+
fix: 'Replace new Date() with sql`now()` for database-server time consistency',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (inSetBlock) {
|
|
78
|
+
for (const ch of line) {
|
|
79
|
+
if (ch === '{') braceDepth++;
|
|
80
|
+
if (ch === '}') braceDepth--;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (/new\s+Date\s*\(\s*\)/.test(line)) {
|
|
84
|
+
findings.push({
|
|
85
|
+
severity: 'block',
|
|
86
|
+
rule: 'no-new-date-in-set',
|
|
87
|
+
line: i + 1,
|
|
88
|
+
message: 'new Date() in .set() block — use sql`now()` for consistent DB timestamps',
|
|
89
|
+
fix: 'Replace new Date() with sql`now()` for database-server time consistency',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (braceDepth <= 0) {
|
|
94
|
+
inSetBlock = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return findings;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Billing-related variable names that indicate inline math */
|
|
103
|
+
const BILLING_VARS = [
|
|
104
|
+
'quantity', 'rate', 'price', 'cost', 'amount',
|
|
105
|
+
'subtotal', 'discount', 'tax', 'vat', 'total',
|
|
106
|
+
'lineTotal', 'grandTotal', 'unitPrice',
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Detect inline billing math that should use shared calculation utilities.
|
|
111
|
+
* When billing calculations are scattered across files, bugs like missing VAT
|
|
112
|
+
* or wrong discount logic slip through.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} filePath
|
|
115
|
+
* @param {string} content
|
|
116
|
+
* @returns {Array}
|
|
117
|
+
*/
|
|
118
|
+
function checkInlineBillingMath(filePath, content) {
|
|
119
|
+
if (isTestFile(filePath)) return [];
|
|
120
|
+
|
|
121
|
+
// Allow math in calculation utility files
|
|
122
|
+
const fileBase = filePath.toLowerCase();
|
|
123
|
+
if (fileBase.includes('calculation') || fileBase.includes('calc.') ||
|
|
124
|
+
fileBase.includes('calc/') || fileBase.includes('-calc')) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const findings = [];
|
|
129
|
+
const lines = content.split('\n');
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < lines.length; i++) {
|
|
132
|
+
const line = lines[i];
|
|
133
|
+
const trimmed = line.trim();
|
|
134
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
|
|
135
|
+
|
|
136
|
+
// Check for billing-variable arithmetic: quantity * rate, subtotal - discount, etc.
|
|
137
|
+
for (const varName of BILLING_VARS) {
|
|
138
|
+
const pattern = new RegExp(`\\b${varName}\\b\\s*[*+\\-]\\s*\\b(${BILLING_VARS.join('|')})\\b`);
|
|
139
|
+
if (pattern.test(line)) {
|
|
140
|
+
findings.push({
|
|
141
|
+
severity: 'warn',
|
|
142
|
+
rule: 'no-inline-billing-math',
|
|
143
|
+
line: i + 1,
|
|
144
|
+
message: 'Inline billing calculation — use shared calculation utility',
|
|
145
|
+
fix: 'Use calculateLineTotal(), calculateSubtotal(), or calculateGrandTotal() from billing-calculations module',
|
|
146
|
+
});
|
|
147
|
+
break; // One finding per line
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return findings;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
checkNewDateInSet,
|
|
157
|
+
checkInlineBillingMath,
|
|
158
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Rules Tests
|
|
3
|
+
*
|
|
4
|
+
* Detects new Date() in ORM .set() blocks and inline billing math.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
checkNewDateInSet,
|
|
10
|
+
checkInlineBillingMath,
|
|
11
|
+
} = require('./database-rules.js');
|
|
12
|
+
|
|
13
|
+
describe('Database Rules', () => {
|
|
14
|
+
describe('checkNewDateInSet', () => {
|
|
15
|
+
it('detects new Date() in .set({}) block', () => {
|
|
16
|
+
const code = `
|
|
17
|
+
db.update(leads).set({
|
|
18
|
+
status: "active",
|
|
19
|
+
updatedAt: new Date(),
|
|
20
|
+
});
|
|
21
|
+
`;
|
|
22
|
+
const findings = checkNewDateInSet('src/leads/leads.service.ts', code);
|
|
23
|
+
expect(findings).toHaveLength(1);
|
|
24
|
+
expect(findings[0].severity).toBe('block');
|
|
25
|
+
expect(findings[0].rule).toBe('no-new-date-in-set');
|
|
26
|
+
expect(findings[0].fix).toContain('sql');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('detects new Date() in .set() with convertedAt', () => {
|
|
30
|
+
const code = `
|
|
31
|
+
db.update(leads).set({
|
|
32
|
+
convertedAt: new Date(),
|
|
33
|
+
updatedAt: new Date(),
|
|
34
|
+
});
|
|
35
|
+
`;
|
|
36
|
+
const findings = checkNewDateInSet('src/leads/leads.service.ts', code);
|
|
37
|
+
expect(findings.length).toBeGreaterThanOrEqual(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('allows new Date() in logging context', () => {
|
|
41
|
+
const code = `
|
|
42
|
+
console.log('Started at', new Date());
|
|
43
|
+
const timestamp = new Date();
|
|
44
|
+
`;
|
|
45
|
+
const findings = checkNewDateInSet('src/utils/logger.ts', code);
|
|
46
|
+
expect(findings).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('allows sql`now()` in .set()', () => {
|
|
50
|
+
const code = `
|
|
51
|
+
db.update(leads).set({
|
|
52
|
+
status: "active",
|
|
53
|
+
updatedAt: sql\`now()\`,
|
|
54
|
+
});
|
|
55
|
+
`;
|
|
56
|
+
const findings = checkNewDateInSet('src/leads/leads.service.ts', code);
|
|
57
|
+
expect(findings).toHaveLength(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('skips test files', () => {
|
|
61
|
+
const code = 'db.update(leads).set({ updatedAt: new Date() });';
|
|
62
|
+
const findings = checkNewDateInSet('src/leads/leads.test.ts', code);
|
|
63
|
+
expect(findings).toHaveLength(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('detects across multiline .set() call', () => {
|
|
67
|
+
const code = [
|
|
68
|
+
'await db',
|
|
69
|
+
' .update(invoices)',
|
|
70
|
+
' .set({',
|
|
71
|
+
' paidAt: new Date(),',
|
|
72
|
+
' status: "paid",',
|
|
73
|
+
' });',
|
|
74
|
+
].join('\n');
|
|
75
|
+
const findings = checkNewDateInSet('src/invoices/invoices.service.ts', code);
|
|
76
|
+
expect(findings).toHaveLength(1);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('checkInlineBillingMath', () => {
|
|
81
|
+
it('detects quantity * rate pattern', () => {
|
|
82
|
+
const code = 'const lineTotal = quantity * rate;';
|
|
83
|
+
const findings = checkInlineBillingMath('src/invoices/editor.tsx', code);
|
|
84
|
+
expect(findings).toHaveLength(1);
|
|
85
|
+
expect(findings[0].severity).toBe('warn');
|
|
86
|
+
expect(findings[0].rule).toBe('no-inline-billing-math');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('detects subtotal - discount pattern', () => {
|
|
90
|
+
const code = 'const total = subtotal - discount + tax;';
|
|
91
|
+
const findings = checkInlineBillingMath('src/invoices/sheet.tsx', code);
|
|
92
|
+
expect(findings).toHaveLength(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('allows math in calculation utility files', () => {
|
|
96
|
+
const code = 'const lineTotal = quantity * rate;';
|
|
97
|
+
const findings = checkInlineBillingMath('src/lib/billing-calculations.ts', code);
|
|
98
|
+
expect(findings).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('allows math in files with "calc" in name', () => {
|
|
102
|
+
const code = 'const total = subtotal - discount;';
|
|
103
|
+
const findings = checkInlineBillingMath('src/utils/price-calc.ts', code);
|
|
104
|
+
expect(findings).toHaveLength(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('skips test files', () => {
|
|
108
|
+
const code = 'const lineTotal = quantity * rate;';
|
|
109
|
+
const findings = checkInlineBillingMath('src/invoices/editor.test.tsx', code);
|
|
110
|
+
expect(findings).toHaveLength(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('allows non-billing arithmetic', () => {
|
|
114
|
+
const code = 'const area = width * height;';
|
|
115
|
+
const findings = checkInlineBillingMath('src/utils/geometry.ts', code);
|
|
116
|
+
expect(findings).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|