chati-dev 4.0.11 → 4.1.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 (107) hide show
  1. package/README.md +5 -0
  2. package/bin/chati.js +16 -0
  3. package/framework/agents/build/dev.md +43 -10
  4. package/framework/agents/discover/brief.md +38 -8
  5. package/framework/agents/discover/brownfield-wu.md +44 -3
  6. package/framework/agents/discover/greenfield-wu.md +14 -0
  7. package/framework/agents/plan/architect.md +6 -0
  8. package/framework/agents/plan/detail.md +25 -0
  9. package/framework/agents/plan/tasks.md +29 -1
  10. package/framework/agents/plan/ux-brand-architect.md +215 -0
  11. package/framework/agents/plan/ux-component-engineer.md +289 -0
  12. package/framework/agents/plan/ux-researcher.md +166 -0
  13. package/framework/agents/plan/ux.md +126 -1008
  14. package/framework/agents/quality/qa-implementation.md +121 -22
  15. package/framework/agents/quality/qa-planning.md +18 -0
  16. package/framework/config.yaml +15 -4
  17. package/framework/constitution.md +8 -2
  18. package/framework/context/root.md +1 -1
  19. package/framework/data/entity-registry.yaml +59 -3
  20. package/framework/hooks/constitution-guard.js +67 -1
  21. package/framework/hooks/license-guard.js +4 -4
  22. package/framework/hooks/model-governance.js +2 -1
  23. package/framework/hooks/prism-engine.js +74 -6
  24. package/framework/hooks/read-protection.js +1 -1
  25. package/framework/hooks/session-digest.js +159 -7
  26. package/framework/hooks/settings.json +16 -4
  27. package/framework/hooks/style-guard.js +134 -0
  28. package/framework/hooks/undercover-guard.js +220 -0
  29. package/framework/intelligence/context-engine.md +21 -0
  30. package/framework/intelligence/hooks-performance.md +54 -0
  31. package/framework/intelligence/memory-layer.md +47 -0
  32. package/framework/migrations/v4.0-to-v4.1.yaml +165 -0
  33. package/framework/orchestrator/chati.md +327 -1067
  34. package/framework/templates/brandbook-html-tmpl.md +107 -0
  35. package/framework/templates/session-memory-tmpl.yaml +68 -0
  36. package/package.json +1 -1
  37. package/src/autonomy/build-state.js +1 -1
  38. package/src/autonomy/worktree-manager.js +13 -13
  39. package/src/config/agent-customizer.js +1 -1
  40. package/src/config/gemini-hooks-generator.js +6 -6
  41. package/src/config/ide-configs.js +1 -1
  42. package/src/context/bracket-tracker.js +25 -4
  43. package/src/context/engine.js +37 -7
  44. package/src/context/formatter.js +45 -1
  45. package/src/context/index.js +3 -3
  46. package/src/dashboard/data-reader.js +7 -7
  47. package/src/decision/engine.js +2 -2
  48. package/src/decision/registry-healer.js +1 -1
  49. package/src/decision/registry-updater.js +2 -2
  50. package/src/extensions/loader.js +1 -1
  51. package/src/gates/g1-planning-complete.js +1 -1
  52. package/src/gates/g2-qa-planning.js +1 -1
  53. package/src/gates/g3-implementation.js +4 -4
  54. package/src/gates/g4-qa-implementation.js +2 -2
  55. package/src/health/auto-fix.js +3 -3
  56. package/src/health/engine.js +2 -2
  57. package/src/installer/brownfield-upgrader.js +3 -3
  58. package/src/installer/manifest.js +1 -1
  59. package/src/installer/transaction.js +1 -1
  60. package/src/installer/validator.js +2 -2
  61. package/src/intelligence/decision-engine.js +1 -1
  62. package/src/intelligence/memory-manager.js +1 -1
  63. package/src/intelligence/registry-manager.js +2 -2
  64. package/src/intelligence/timeline.js +1 -1
  65. package/src/license/client.js +1 -1
  66. package/src/license/commands.js +2 -2
  67. package/src/memory/agent-memory.js +3 -3
  68. package/src/memory/daily-digest.js +170 -0
  69. package/src/memory/dream.js +254 -0
  70. package/src/memory/gotchas.js +2 -2
  71. package/src/memory/index.js +18 -0
  72. package/src/memory/magic-docs.js +98 -0
  73. package/src/memory/memory-extractor.js +163 -0
  74. package/src/memory/session-digest.js +144 -6
  75. package/src/merger/yaml-merger.js +1 -1
  76. package/src/orchestrator/cli.js +980 -0
  77. package/src/orchestrator/handoff-engine.js +25 -4
  78. package/src/orchestrator/index.js +9 -0
  79. package/src/orchestrator/session-manager.js +1 -1
  80. package/src/preview/detector.js +3 -3
  81. package/src/preview/launcher.js +2 -2
  82. package/src/quality/metrics-collector.js +1 -1
  83. package/src/quality/test-runner.js +2 -4
  84. package/src/scanning/density-scanner.js +51 -0
  85. package/src/scanning/env-scanner.js +97 -0
  86. package/src/scanning/index.js +7 -0
  87. package/src/scanning/leakage-scanner.js +54 -0
  88. package/src/scanning/placeholder-scanner.js +40 -0
  89. package/src/scanning/security-scanner.js +94 -0
  90. package/src/security/bash-security.js +335 -0
  91. package/src/security/index.js +9 -0
  92. package/src/telemetry/config.js +3 -3
  93. package/src/telemetry/sender.js +1 -1
  94. package/src/terminal/cli-registry.js +1 -1
  95. package/src/terminal/cost-tracker.js +1 -2
  96. package/src/terminal/prompt-builder.js +11 -27
  97. package/src/terminal/rate-limiter.js +1 -2
  98. package/src/terminal/run-agent.js +3 -3
  99. package/src/terminal/spawner.js +41 -4
  100. package/src/upgrade/checker.js +2 -2
  101. package/src/upgrade/migrator.js +1 -1
  102. package/src/utils/config-parser.js +1 -1
  103. package/src/utils/file-lock.js +3 -3
  104. package/src/utils/provider-limits.js +35 -4
  105. package/src/wizard/i18n.js +2 -2
  106. package/src/wizard/index.js +2 -1
  107. package/src/wizard/questions.js +4 -2
@@ -8,6 +8,7 @@
8
8
  import { buildHandoff, saveHandoff, loadHandoff } from '../tasks/handoff.js';
9
9
  import { readAgentMemory } from '../memory/agent-memory.js';
10
10
  import { getRelevantGotchas } from '../memory/gotchas.js';
11
+ import { updateClaudeMd } from '../memory/magic-docs.js';
11
12
  import { existsSync, readdirSync } from 'fs';
12
13
  import { join } from 'path';
13
14
 
@@ -65,6 +66,23 @@ export function executeHandoff(projectDir, params) {
65
66
  };
66
67
  }
67
68
 
69
+ // Auto-update CLAUDE.md with current state (Magic Docs)
70
+ try {
71
+ const agentName = params.fromTask?.agent || params.fromTask?.name || params.fromTask?.id?.split('-')[0] || 'agent';
72
+ updateClaudeMd(projectDir, {
73
+ currentAgent: agentName,
74
+ pipelinePosition: handoff.status || 'in_progress',
75
+ activeTask: params.fromTask?.title || params.fromTask?.id || null,
76
+ progress: params.validation?.score || null,
77
+ decisions: params.decisions
78
+ ? Object.entries(params.decisions).map(([what, why]) => ({ what, why: String(why) }))
79
+ : [],
80
+ nextSteps: params.blockers && params.blockers.length > 0
81
+ ? [`Resolve ${params.blockers.length} blocker(s)`]
82
+ : ['Continue to next agent'],
83
+ });
84
+ } catch { /* Magic Docs update is non-critical */ }
85
+
68
86
  return {
69
87
  success: true,
70
88
  handoff,
@@ -156,9 +174,12 @@ export function loadHandoffContext(projectDir, fromAgent) {
156
174
  // Load agent memories
157
175
  const memoryResult = readAgentMemory(projectDir, fromAgent);
158
176
  if (memoryResult.loaded && memoryResult.entries.length > 0) {
159
- context.memories = memoryResult.entries.filter(
160
- e => e.confidence === 'high' || e.confidence === 'medium'
161
- );
177
+ context.memories = memoryResult.entries.filter(e => {
178
+ // Support both legacy string confidence ("high"/"medium") and
179
+ // numeric confidence (0.0-1.0) from memory-extractor
180
+ if (typeof e.confidence === 'number') return e.confidence >= 0.5;
181
+ return e.confidence === 'high' || e.confidence === 'medium';
182
+ });
162
183
  sources.push(`${context.memories.length} memory entries from ${fromAgent}`);
163
184
  }
164
185
 
@@ -217,7 +238,7 @@ export function getHandoffHistory(projectDir) {
217
238
  files.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
218
239
 
219
240
  return files;
220
- } catch {
241
+ } catch { /* expected: operation may fail gracefully */
221
242
  return [];
222
243
  }
223
244
  }
@@ -6,6 +6,8 @@
6
6
  export {
7
7
  INTENT_TYPES,
8
8
  classifyIntent,
9
+ detectQuickFlow,
10
+ detectStandardFlow,
9
11
  getIntentPhase,
10
12
  checkModeAlignment,
11
13
  } from './intent-classifier.js';
@@ -24,17 +26,21 @@ export {
24
26
  PIPELINE_PHASES,
25
27
  AGENT_STATUS,
26
28
  initPipeline,
29
+ initQuickFlowPipeline,
30
+ initStandardFlowPipeline,
27
31
  advancePipeline,
28
32
  checkPhaseTransition,
29
33
  getPipelineProgress,
30
34
  resetPipelineTo,
31
35
  isPipelineComplete,
32
36
  markAgentInProgress,
37
+ confirmPreview,
33
38
  } from './pipeline-manager.js';
34
39
 
35
40
  export {
36
41
  executeHandoff,
37
42
  validateHandoffPreconditions,
43
+ validateHandoffIntegrity,
38
44
  loadHandoffContext,
39
45
  getHandoffHistory,
40
46
  checkRollbackFeasibility,
@@ -56,4 +62,7 @@ export {
56
62
  recordAgentCompletion,
57
63
  getSessionSummary,
58
64
  validateSession,
65
+ claimSession,
66
+ releaseSession,
67
+ getSessionOwner,
59
68
  } from './session-manager.js';
@@ -159,7 +159,7 @@ export function loadSession(projectDir) {
159
159
  try {
160
160
  const yamlContent = yaml.dump(session, { lineWidth: -1, noRefs: true });
161
161
  writeFileSync(sessionPath, yamlContent, 'utf-8');
162
- } catch {
162
+ } catch { /* expected: operation may fail gracefully */
163
163
  // Migration write failed — continue with migrated in-memory session
164
164
  }
165
165
  }
@@ -107,7 +107,7 @@ export function detectDevCommand(projectDir) {
107
107
  };
108
108
  }
109
109
  }
110
- } catch {
110
+ } catch { /* expected: operation may fail gracefully */
111
111
  // Invalid package.json — skip
112
112
  }
113
113
  }
@@ -164,7 +164,7 @@ export function detectProjectKind(projectDir) {
164
164
  if ((pkg.main || pkg.exports) && !pkg.scripts?.dev && !pkg.scripts?.start) {
165
165
  return 'library';
166
166
  }
167
- } catch {
167
+ } catch { /* expected: operation may fail gracefully */
168
168
  // Invalid package.json
169
169
  }
170
170
  }
@@ -222,7 +222,7 @@ function resolveFrameworkCommand(projectDir, detector) {
222
222
  };
223
223
  }
224
224
  }
225
- } catch {
225
+ } catch { /* expected: operation may fail gracefully */
226
226
  // Fall through to default
227
227
  }
228
228
  }
@@ -134,7 +134,7 @@ function killProcess(proc) {
134
134
  const forceKillTimer = setTimeout(() => {
135
135
  try {
136
136
  proc.kill('SIGKILL');
137
- } catch {
137
+ } catch { /* expected: process may already be dead */
138
138
  // Already dead
139
139
  }
140
140
  resolve();
@@ -147,7 +147,7 @@ function killProcess(proc) {
147
147
 
148
148
  try {
149
149
  proc.kill('SIGTERM');
150
- } catch {
150
+ } catch { /* expected: process may already be dead */
151
151
  clearTimeout(forceKillTimer);
152
152
  resolve();
153
153
  }
@@ -70,7 +70,7 @@ function loadMetrics(projectDir) {
70
70
  const raw = readFileSync(filePath, 'utf-8');
71
71
  const data = JSON.parse(raw);
72
72
  return { metrics: Array.isArray(data.metrics) ? data.metrics : [] };
73
- } catch {
73
+ } catch { /* expected: file may not exist */
74
74
  return { metrics: [] };
75
75
  }
76
76
  }
@@ -47,8 +47,7 @@ export function detectTestCommand(projectDir) {
47
47
  const runner = detectRunnerFromScript(testScript);
48
48
  return { command: 'npm test', runner, detected: true };
49
49
  }
50
- } catch {
51
- // Malformed package.json — continue detection
50
+ } catch { /* expected: package.json may be malformed or missing */
52
51
  }
53
52
  }
54
53
 
@@ -198,8 +197,7 @@ export function runLint(projectDir, options = {}) {
198
197
  if (pkg.scripts?.lint) {
199
198
  command = 'npm run lint';
200
199
  }
201
- } catch {
202
- // ignore
200
+ } catch { /* expected: package.json may be malformed */
203
201
  }
204
202
  }
205
203
  }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Information Density Scanner -- Detects LLM-generated filler phrases
3
+ * that dilute requirements in planning artifacts.
4
+ * Deterministic regex scan. Zero LLM dependency.
5
+ */
6
+
7
+ import { readFileSync } from 'fs';
8
+
9
+ const FILLER_PATTERNS = [
10
+ { pattern: /it is important to note that/gi, suggestion: 'Remove — state the fact directly' },
11
+ { pattern: /it(?:'s| is) worth (?:noting|mentioning)/gi, suggestion: 'Remove — state directly' },
12
+ { pattern: /\bfurthermore\b/gi, suggestion: 'Remove or replace with specific connector' },
13
+ { pattern: /\bmoreover\b/gi, suggestion: 'Remove or replace with specific connector' },
14
+ { pattern: /\bin conclusion\b/gi, suggestion: 'Remove — let content conclude itself' },
15
+ { pattern: /as (?:mentioned|noted|stated) (?:earlier|above|before|previously)/gi, suggestion: 'Remove — reader has context' },
16
+ { pattern: /needless to say/gi, suggestion: 'Remove — if needless, do not say it' },
17
+ { pattern: /it goes without saying/gi, suggestion: 'Remove' },
18
+ { pattern: /at the end of the day/gi, suggestion: 'Replace with specific conclusion' },
19
+ { pattern: /\bin order to\b/gi, suggestion: 'Replace with "to"' },
20
+ { pattern: /\butilize\b/gi, suggestion: 'Replace with "use"' },
21
+ { pattern: /\bleverage\b/gi, suggestion: 'Replace with "use"' },
22
+ { pattern: /\bfacilitate\b/gi, suggestion: 'Replace with simpler verb' },
23
+ { pattern: /\bdelve into\b/gi, suggestion: 'Replace with "explore" or "examine"' },
24
+ { pattern: /\bdive into\b/gi, suggestion: 'Replace with "examine" or remove' },
25
+ { pattern: /\brobust\b/gi, suggestion: 'Be specific — what makes it robust?' },
26
+ { pattern: /\bseamless(?:ly)?\b/gi, suggestion: 'Be specific — what makes it seamless?' },
27
+ { pattern: /\bcomprehensive\b/gi, suggestion: 'Be specific — what does it cover?' },
28
+ ];
29
+
30
+ /**
31
+ * Scan a file for LLM-generated filler phrases.
32
+ * @param {string} filePath - Path to file to scan
33
+ * @returns {{ clean: boolean, findings: Array<{phrase: string, line: number, suggestion: string}>, count: number }}
34
+ */
35
+ export function scanInformationDensity(filePath) {
36
+ const content = readFileSync(filePath, 'utf-8');
37
+ const findings = [];
38
+
39
+ for (const { pattern, suggestion } of FILLER_PATTERNS) {
40
+ const re = new RegExp(pattern.source, pattern.flags);
41
+ let match;
42
+ while ((match = re.exec(content)) !== null) {
43
+ const line = content.slice(0, match.index).split('\n').length;
44
+ findings.push({ phrase: match[0], line, suggestion });
45
+ }
46
+ }
47
+
48
+ return { clean: findings.length === 0, findings, count: findings.length };
49
+ }
50
+
51
+ export { FILLER_PATTERNS };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Environment Variable Scanner -- Cross-checks .env.example against
3
+ * actual env var usage in source code.
4
+ * Deterministic file comparison. Zero LLM dependency.
5
+ */
6
+
7
+ import { readFileSync, existsSync, readdirSync } from 'fs';
8
+ import { join, extname } from 'path';
9
+
10
+ const ENV_USAGE_PATTERNS = [
11
+ /process\.env\.(\w+)/g,
12
+ /import\.meta\.env\.(\w+)/g,
13
+ /Deno\.env\.get\(['"](\w+)['"]\)/g,
14
+ ];
15
+
16
+ const SOURCE_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py']);
17
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.chati', 'chati.dev', 'dist', 'build', '.next']);
18
+
19
+ /**
20
+ * Parse .env.example for declared variable names.
21
+ */
22
+ function parseEnvFile(filePath) {
23
+ if (!existsSync(filePath)) return [];
24
+ const content = readFileSync(filePath, 'utf-8');
25
+ const vars = [];
26
+ for (const line of content.split('\n')) {
27
+ const trimmed = line.trim();
28
+ if (!trimmed || trimmed.startsWith('#')) continue;
29
+ const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)\s*=/);
30
+ if (match) vars.push(match[1]);
31
+ }
32
+ return vars;
33
+ }
34
+
35
+ /**
36
+ * Recursively find all source files.
37
+ */
38
+ function findSourceFiles(dir, files = []) {
39
+ if (!existsSync(dir)) return files;
40
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
41
+ if (SKIP_DIRS.has(entry.name)) continue;
42
+ const fullPath = join(dir, entry.name);
43
+ if (entry.isDirectory()) {
44
+ findSourceFiles(fullPath, files);
45
+ } else if (SOURCE_EXTENSIONS.has(extname(entry.name))) {
46
+ files.push(fullPath);
47
+ }
48
+ }
49
+ return files;
50
+ }
51
+
52
+ /**
53
+ * Grep all source files for env var usage.
54
+ */
55
+ function grepEnvUsage(projectDir) {
56
+ const files = findSourceFiles(projectDir);
57
+ const usedVars = new Set();
58
+
59
+ for (const file of files) {
60
+ try {
61
+ const content = readFileSync(file, 'utf-8');
62
+ for (const pattern of ENV_USAGE_PATTERNS) {
63
+ const re = new RegExp(pattern.source, pattern.flags);
64
+ let match;
65
+ while ((match = re.exec(content)) !== null) {
66
+ usedVars.add(match[1]);
67
+ }
68
+ }
69
+ } catch { /* expected: file may be unreadable */ }
70
+ }
71
+
72
+ return [...usedVars];
73
+ }
74
+
75
+ /**
76
+ * Scan for env var synchronization issues.
77
+ * @param {string} projectDir - Project root
78
+ * @returns {{ synced: boolean, missing: string[], extra: string[], declared: number, used: number }}
79
+ */
80
+ export function scanEnvSync(projectDir) {
81
+ const envExamplePath = join(projectDir, '.env.example');
82
+ const declared = parseEnvFile(envExamplePath);
83
+ const used = grepEnvUsage(projectDir);
84
+
85
+ const missing = used.filter(v => !declared.includes(v));
86
+ const extra = declared.filter(v => !used.includes(v));
87
+
88
+ return {
89
+ synced: missing.length === 0,
90
+ missing,
91
+ extra,
92
+ declared: declared.length,
93
+ used: used.length,
94
+ };
95
+ }
96
+
97
+ export { parseEnvFile, grepEnvUsage };
@@ -0,0 +1,7 @@
1
+ // Barrel exports for scanning system
2
+
3
+ export { scanPlaceholders } from './placeholder-scanner.js';
4
+ export { scanImplementationLeakage, extractFRSection, TECH_NAMES } from './leakage-scanner.js';
5
+ export { scanInformationDensity, FILLER_PATTERNS } from './density-scanner.js';
6
+ export { scanEnvSync, parseEnvFile, grepEnvUsage } from './env-scanner.js';
7
+ export { scanProjectSecurity, detectLanguage } from './security-scanner.js';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Implementation Leakage Scanner -- Detects technology names inside
3
+ * Functional Requirements (FRs) where only WHAT should be specified, not HOW.
4
+ * Deterministic regex scan. Zero LLM dependency.
5
+ */
6
+
7
+ import { readFileSync } from 'fs';
8
+
9
+ const TECH_NAMES = [
10
+ /\bReact\b/g, /\bNext\.?js\b/gi, /\bVue\.?js\b/gi, /\bAngular\b/g, /\bSvelte\b/g,
11
+ /\bPostgreSQL\b/gi, /\bMongoDB\b/gi, /\bMySQL\b/gi, /\bSupabase\b/gi, /\bFirebase\b/gi,
12
+ /\bTailwind\b/gi, /\bPrisma\b/gi, /\bDrizzle\b/gi, /\bSequelize\b/gi,
13
+ /\bExpress\b/g, /\bFastify\b/g, /\bDjango\b/g, /\bFlask\b/g, /\bLaravel\b/g,
14
+ /\bAWS\b/g, /\bVercel\b/g, /\bNetlify\b/g, /\bRailway\b/g, /\bCloudflare\b/g,
15
+ /\bDocker\b/g, /\bKubernetes\b/g, /\bRedis\b/g, /\bGraphQL\b/gi,
16
+ /\bTypeScript\b/gi, /\bJavaScript\b/gi, /\bPython\b/g, /\bRust\b/g, /\bGo\b/g,
17
+ /\bZod\b/g, /\bJoi\b/g, /\bZustand\b/g, /\bRedux\b/g, /\bJotai\b/g,
18
+ ];
19
+
20
+ /**
21
+ * Extract the Functional Requirements section from a PRD.
22
+ * @param {string} content - Full PRD content
23
+ * @returns {string|null} FR section text, or null if not found
24
+ */
25
+ function extractFRSection(content) {
26
+ const frMatch = content.match(/#{1,3}\s*(?:\d+\.\s*)?Functional Requirements[\s\S]*?(?=#{1,3}\s*(?:\d+\.\s*)?(?:Non-Functional|Business Rules|Risks|Dependencies)|$)/i);
27
+ return frMatch ? frMatch[0] : null;
28
+ }
29
+
30
+ /**
31
+ * Scan a PRD file for technology names inside Functional Requirements.
32
+ * @param {string} filePath - Path to PRD file
33
+ * @returns {{ clean: boolean, findings: Array<{tech: string, context: string}>, count: number }}
34
+ */
35
+ export function scanImplementationLeakage(filePath) {
36
+ const content = readFileSync(filePath, 'utf-8');
37
+ const frSection = extractFRSection(content);
38
+ if (!frSection) return { clean: true, findings: [], count: 0, note: 'No FR section found' };
39
+
40
+ const findings = [];
41
+ for (const pattern of TECH_NAMES) {
42
+ const re = new RegExp(pattern.source, pattern.flags);
43
+ let match;
44
+ while ((match = re.exec(frSection)) !== null) {
45
+ const start = Math.max(0, match.index - 40);
46
+ const end = Math.min(frSection.length, match.index + match[0].length + 40);
47
+ findings.push({ tech: match[0], context: frSection.slice(start, end).replace(/\n/g, ' ').trim() });
48
+ }
49
+ }
50
+
51
+ return { clean: findings.length === 0, findings, count: findings.length };
52
+ }
53
+
54
+ export { extractFRSection, TECH_NAMES };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Placeholder Scanner -- Detects [TODO], [TBD], and similar placeholders in artifacts.
3
+ * Deterministic regex scan. Zero LLM dependency.
4
+ */
5
+
6
+ import { readFileSync } from 'fs';
7
+
8
+ const PLACEHOLDER_PATTERNS = [
9
+ /\[TODO\]/gi,
10
+ /\[TBD\]/gi,
11
+ /\{TODO\}/gi,
12
+ /\{TBD\}/gi,
13
+ /\bPLACEHOLDER\b/gi,
14
+ /\bXXX\b/g,
15
+ /\bFIXME\b/g,
16
+ /\bHACK\b/g,
17
+ /\[INSERT\b/gi,
18
+ /\bTBA\b/g,
19
+ ];
20
+
21
+ /**
22
+ * Scan a file for placeholder patterns.
23
+ * @param {string} filePath - Path to file to scan
24
+ * @returns {{ clean: boolean, findings: Array<{pattern: string, line: number}>, count: number }}
25
+ */
26
+ export function scanPlaceholders(filePath) {
27
+ const content = readFileSync(filePath, 'utf-8');
28
+ const findings = [];
29
+
30
+ for (const pattern of PLACEHOLDER_PATTERNS) {
31
+ const re = new RegExp(pattern.source, pattern.flags);
32
+ let match;
33
+ while ((match = re.exec(content)) !== null) {
34
+ const line = content.slice(0, match.index).split('\n').length;
35
+ findings.push({ pattern: match[0], line });
36
+ }
37
+ }
38
+
39
+ return { clean: findings.length === 0, findings, count: findings.length };
40
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Security Scanner -- Scans project source files for language-specific
3
+ * security vulnerabilities using regex patterns.
4
+ * Uses LANGUAGE_SECURITY_CHECKS from bash-security.js.
5
+ * Deterministic file scanning. Zero LLM dependency.
6
+ */
7
+
8
+ import { readFileSync, readdirSync, existsSync } from 'fs';
9
+ import { join, extname, relative } from 'path';
10
+ import { runLanguageSecurityChecks } from '../security/bash-security.js';
11
+
12
+ const LANGUAGE_MAP = {
13
+ '.js': 'javascript',
14
+ '.jsx': 'javascript',
15
+ '.mjs': 'javascript',
16
+ '.cjs': 'javascript',
17
+ '.ts': 'typescript',
18
+ '.tsx': 'typescript',
19
+ '.py': 'python',
20
+ };
21
+
22
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.chati', 'chati.dev', 'dist', 'build', '.next', 'coverage', '__pycache__']);
23
+
24
+ /**
25
+ * Recursively find source files by language.
26
+ */
27
+ function findFiles(dir, language, files = []) {
28
+ if (!existsSync(dir)) return files;
29
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
30
+ if (SKIP_DIRS.has(entry.name)) continue;
31
+ const fullPath = join(dir, entry.name);
32
+ if (entry.isDirectory()) {
33
+ findFiles(fullPath, language, files);
34
+ } else {
35
+ const ext = extname(entry.name);
36
+ const lang = LANGUAGE_MAP[ext];
37
+ if (lang && (!language || lang === language)) {
38
+ files.push({ path: fullPath, language: lang });
39
+ }
40
+ }
41
+ }
42
+ return files;
43
+ }
44
+
45
+ /**
46
+ * Detect primary language from project structure.
47
+ */
48
+ export function detectLanguage(projectDir) {
49
+ const pkgPath = join(projectDir, 'package.json');
50
+ if (existsSync(pkgPath)) {
51
+ const tsConfig = join(projectDir, 'tsconfig.json');
52
+ return existsSync(tsConfig) ? 'typescript' : 'javascript';
53
+ }
54
+ const reqPath = join(projectDir, 'requirements.txt');
55
+ if (existsSync(reqPath)) return 'python';
56
+ return 'javascript';
57
+ }
58
+
59
+ /**
60
+ * Scan project source files for security vulnerabilities.
61
+ * @param {string} projectDir - Project root
62
+ * @param {object} [options={}]
63
+ * @param {string} [options.language] - Filter by language
64
+ * @param {string} [options.dir] - Subdirectory to scan (default: project root)
65
+ * @returns {{ scanned: number, safe: boolean, findings: Array, critical: number, high: number, medium: number }}
66
+ */
67
+ export function scanProjectSecurity(projectDir, options = {}) {
68
+ const language = options.language || null;
69
+ const scanDir = options.dir ? join(projectDir, options.dir) : projectDir;
70
+
71
+ const files = findFiles(scanDir, language);
72
+ const allFindings = [];
73
+
74
+ for (const file of files) {
75
+ try {
76
+ const content = readFileSync(file.path, 'utf-8');
77
+ const result = runLanguageSecurityChecks(content, file.language);
78
+ if (!result.safe) {
79
+ for (const f of result.findings) {
80
+ allFindings.push({ ...f, file: relative(projectDir, file.path), language: file.language });
81
+ }
82
+ }
83
+ } catch { /* expected: file may be unreadable */ }
84
+ }
85
+
86
+ return {
87
+ scanned: files.length,
88
+ safe: allFindings.length === 0,
89
+ findings: allFindings,
90
+ critical: allFindings.filter(f => f.severity === 'critical').length,
91
+ high: allFindings.filter(f => f.severity === 'high').length,
92
+ medium: allFindings.filter(f => f.severity === 'medium').length,
93
+ };
94
+ }