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,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
+ });