sigmap 1.5.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 (44) hide show
  1. package/.contextignore.example +34 -0
  2. package/CHANGELOG.md +402 -0
  3. package/LICENSE +21 -0
  4. package/README.md +601 -0
  5. package/gen-context.config.json.example +40 -0
  6. package/gen-context.js +4316 -0
  7. package/gen-project-map.js +172 -0
  8. package/package.json +67 -0
  9. package/src/config/defaults.js +61 -0
  10. package/src/config/loader.js +60 -0
  11. package/src/extractors/cpp.js +60 -0
  12. package/src/extractors/csharp.js +48 -0
  13. package/src/extractors/css.js +51 -0
  14. package/src/extractors/dart.js +58 -0
  15. package/src/extractors/dockerfile.js +49 -0
  16. package/src/extractors/go.js +61 -0
  17. package/src/extractors/html.js +39 -0
  18. package/src/extractors/java.js +49 -0
  19. package/src/extractors/javascript.js +82 -0
  20. package/src/extractors/kotlin.js +62 -0
  21. package/src/extractors/php.js +62 -0
  22. package/src/extractors/python.js +69 -0
  23. package/src/extractors/ruby.js +43 -0
  24. package/src/extractors/rust.js +72 -0
  25. package/src/extractors/scala.js +67 -0
  26. package/src/extractors/shell.js +43 -0
  27. package/src/extractors/svelte.js +51 -0
  28. package/src/extractors/swift.js +63 -0
  29. package/src/extractors/typescript.js +109 -0
  30. package/src/extractors/vue.js +66 -0
  31. package/src/extractors/yaml.js +59 -0
  32. package/src/format/cache.js +53 -0
  33. package/src/health/scorer.js +123 -0
  34. package/src/map/class-hierarchy.js +117 -0
  35. package/src/map/import-graph.js +148 -0
  36. package/src/map/route-table.js +127 -0
  37. package/src/mcp/handlers.js +433 -0
  38. package/src/mcp/server.js +128 -0
  39. package/src/mcp/tools.js +125 -0
  40. package/src/routing/classifier.js +102 -0
  41. package/src/routing/hints.js +103 -0
  42. package/src/security/patterns.js +51 -0
  43. package/src/security/scanner.js +36 -0
  44. package/src/tracking/logger.js +115 -0
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Classify files by complexity tier for model routing hints.
5
+ *
6
+ * Tiers:
7
+ * 'fast' — simple files: config, markup, templates, trivial utilities
8
+ * 'balanced' — standard application code: moderate functions and classes
9
+ * 'powerful' — complex files: large classes, many exports, security-critical paths
10
+ *
11
+ * @param {string} filePath - absolute path to the file
12
+ * @param {string[]} sigs - extracted signatures for the file
13
+ * @returns {'fast'|'balanced'|'powerful'}
14
+ */
15
+ function classify(filePath, sigs) {
16
+ const lower = filePath.toLowerCase();
17
+ const sigCount = sigs.length;
18
+
19
+ // ── Fast tier heuristics ────────────────────────────────────────────────
20
+ // Configuration, markup, templates, and trivial utilities are small tasks
21
+ if (
22
+ lower.endsWith('.json') ||
23
+ lower.endsWith('.yml') ||
24
+ lower.endsWith('.yaml') ||
25
+ lower.endsWith('.toml') ||
26
+ lower.endsWith('.env') ||
27
+ lower.endsWith('.html') ||
28
+ lower.endsWith('.htm') ||
29
+ lower.endsWith('.css') ||
30
+ lower.endsWith('.scss') ||
31
+ lower.endsWith('.sass') ||
32
+ lower.endsWith('.less') ||
33
+ /dockerfile/i.test(lower) ||
34
+ lower.endsWith('.sh') ||
35
+ lower.endsWith('.bash') ||
36
+ lower.endsWith('.zsh') ||
37
+ lower.endsWith('.fish')
38
+ ) {
39
+ return 'fast';
40
+ }
41
+
42
+ // Config-like directories
43
+ if (
44
+ lower.includes('/config/') ||
45
+ lower.includes('/configs/') ||
46
+ lower.includes('/fixtures/') ||
47
+ lower.includes('/migrations/') ||
48
+ lower.includes('/seeds/')
49
+ ) {
50
+ return sigCount > 4 ? 'balanced' : 'fast';
51
+ }
52
+
53
+ // Test files — usually standard complexity
54
+ if (/\.(test|spec)\.[a-z]+$/.test(lower) || /_test\.[a-z]+$/.test(lower)) {
55
+ return 'balanced';
56
+ }
57
+
58
+ // ── Powerful tier heuristics — high-signal keywords in the path ─────────
59
+ if (
60
+ lower.includes('/security/') ||
61
+ lower.includes('/auth/') ||
62
+ lower.includes('/crypto/') ||
63
+ lower.includes('/core/') ||
64
+ lower.includes('/engine/') ||
65
+ lower.includes('/compiler/') ||
66
+ lower.includes('/parser/') ||
67
+ lower.includes('/scheduler/') ||
68
+ lower.includes('/orchestrat')
69
+ ) {
70
+ return 'powerful';
71
+ }
72
+
73
+ // Many exports → complex file
74
+ if (sigCount >= 12) return 'powerful';
75
+
76
+ // Large number of class-level methods (indented 2-space sigs)
77
+ const methodCount = sigs.filter((s) => s.startsWith(' ')).length;
78
+ if (methodCount >= 8) return 'powerful';
79
+
80
+ // ── Balanced covers everything else ────────────────────────────────────
81
+ if (sigCount <= 2) return 'fast';
82
+ return 'balanced';
83
+ }
84
+
85
+ /**
86
+ * Classify all file entries and group them by tier.
87
+ *
88
+ * @param {Array<{filePath: string, sigs: string[]}>} fileEntries
89
+ * @returns {{ fast: string[], balanced: string[], powerful: string[] }}
90
+ * Each array contains relative paths (relative to cwd).
91
+ */
92
+ function classifyAll(fileEntries, cwd) {
93
+ const path = require('path');
94
+ const result = { fast: [], balanced: [], powerful: [] };
95
+ for (const { filePath, sigs } of fileEntries) {
96
+ const tier = classify(filePath, sigs);
97
+ result[tier].push(path.relative(cwd, filePath));
98
+ }
99
+ return result;
100
+ }
101
+
102
+ module.exports = { classify, classifyAll };
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Model routing hint definitions for SigMap.
5
+ *
6
+ * Maps complexity tiers to:
7
+ * - example model names (vendor-agnostic labels used as hints)
8
+ * - task types that belong to each tier
9
+ * - estimated per-1K-token API cost (USD, illustrative — update as needed)
10
+ *
11
+ * These are embedded in the generated context file when `routing: true`
12
+ * so that AI agents and developers know which model tier to invoke.
13
+ */
14
+
15
+ const TIERS = {
16
+ fast: {
17
+ label: 'Fast (low-cost)',
18
+ examples: 'claude-haiku-4-5, gpt-5-1-codex-mini, gemini-3-flash',
19
+ tasks: [
20
+ 'Autocomplete and inline suggestions',
21
+ 'Edit config or markup files',
22
+ 'Fix typos and rename symbols',
23
+ 'Format, lint, and trivial style changes',
24
+ 'Explain a short utility function',
25
+ 'Generate simple shell scripts or Dockerfiles',
26
+ ],
27
+ costHint: '~$0.0008 / 1K tokens',
28
+ },
29
+
30
+ balanced: {
31
+ label: 'Balanced (mid-tier)',
32
+ examples: 'claude-sonnet-4-6, gpt-5-2, gemini-3-1-pro',
33
+ tasks: [
34
+ 'Write unit or integration tests',
35
+ 'Implement a well-scoped feature function',
36
+ 'Debug a runtime error with stack trace',
37
+ 'Refactor a module (< 200 lines)',
38
+ 'Generate a PR description',
39
+ 'Explain a multi-function module',
40
+ ],
41
+ costHint: '~$0.003 / 1K tokens',
42
+ },
43
+
44
+ powerful: {
45
+ label: 'Powerful (high-cost)',
46
+ examples: 'claude-opus-4-6, gpt-5-4, gemini-2-5-pro',
47
+ tasks: [
48
+ 'Cross-cutting architecture decisions',
49
+ 'Multi-file refactor spanning 5+ files',
50
+ 'Security audit (OWASP Top 10)',
51
+ 'Complex debugging across async boundaries',
52
+ 'Migration plan for a library/framework upgrade',
53
+ 'Designing a new module from requirements',
54
+ ],
55
+ costHint: '~$0.015 / 1K tokens',
56
+ },
57
+ };
58
+
59
+ /**
60
+ * Format the routing section as markdown to append to the context file.
61
+ *
62
+ * @param {{ fast: string[], balanced: string[], powerful: string[] }} groups
63
+ * Relative file paths grouped by tier (from classifier.classifyAll).
64
+ * @returns {string} Markdown block to embed in the context output.
65
+ */
66
+ function formatRoutingSection(groups) {
67
+ const lines = [
68
+ '',
69
+ '---',
70
+ '',
71
+ '## Model routing hints',
72
+ '<!-- Generated by SigMap routing module — update gen-context.config.json to disable -->',
73
+ '',
74
+ 'Select the model tier based on the task complexity and the files involved.',
75
+ '',
76
+ ];
77
+
78
+ for (const [tier, info] of Object.entries(TIERS)) {
79
+ const files = groups[tier] || [];
80
+ lines.push(`### ${info.label}`);
81
+ lines.push(`**Examples:** ${info.examples} `);
82
+ lines.push(`**Cost:** ${info.costHint}`);
83
+ lines.push('');
84
+ lines.push('**Use for tasks like:**');
85
+ for (const task of info.tasks) lines.push(`- ${task}`);
86
+ lines.push('');
87
+ if (files.length > 0) {
88
+ lines.push('**Files in this tier:**');
89
+ for (const f of files.slice(0, 15)) lines.push(`- \`${f}\``);
90
+ if (files.length > 15) lines.push(`- … and ${files.length - 15} more`);
91
+ } else {
92
+ lines.push('**Files in this tier:** _(none detected)_');
93
+ }
94
+ lines.push('');
95
+ }
96
+
97
+ lines.push('> **Tip:** Run `node gen-context.js --routing` to regenerate routing hints.');
98
+ lines.push('> See `docs/MODEL_ROUTING.md` for full routing guide and cost optimisation tips.');
99
+
100
+ return lines.join('\n');
101
+ }
102
+
103
+ module.exports = { TIERS, formatRoutingSection };
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Secret detection patterns for SigMap scanner.
5
+ * Each pattern has a name and a regex tested against signature strings.
6
+ */
7
+ const PATTERNS = [
8
+ {
9
+ name: 'AWS Access Key',
10
+ regex: /AKIA[0-9A-Z]{16}/,
11
+ },
12
+ {
13
+ name: 'AWS Secret Key',
14
+ // 40-char base64-like string following common AWS secret key assignment patterns
15
+ regex: /(?:aws_secret|secret_access_key|SecretAccessKey)\s*[:=]\s*['"]?[0-9a-zA-Z/+]{40}/i,
16
+ },
17
+ {
18
+ name: 'GCP API Key',
19
+ regex: /AIza[0-9A-Za-z\-_]{35}/,
20
+ },
21
+ {
22
+ name: 'GitHub Token',
23
+ regex: /gh[pousr]_[A-Za-z0-9_]{36,}/,
24
+ },
25
+ {
26
+ name: 'JWT Token',
27
+ regex: /eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/,
28
+ },
29
+ {
30
+ name: 'DB Connection String',
31
+ regex: /(mongodb|postgres|postgresql|mysql|redis):\/\/[^:]+:[^@]+@/i,
32
+ },
33
+ {
34
+ name: 'SSH Private Key',
35
+ regex: /-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/,
36
+ },
37
+ {
38
+ name: 'Stripe Key',
39
+ regex: /sk_(live|test)_[0-9a-zA-Z]{24,}/,
40
+ },
41
+ {
42
+ name: 'Twilio Key',
43
+ regex: /SK[0-9a-fA-F]{32}/,
44
+ },
45
+ {
46
+ name: 'Generic Secret',
47
+ regex: /(secret|password|passwd|api_key|apikey|auth_token|access_token)\s*[:=]\s*['"][^'"]{8,}['"]/i,
48
+ },
49
+ ];
50
+
51
+ module.exports = { PATTERNS };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const { PATTERNS } = require('./patterns');
4
+
5
+ /**
6
+ * Scan an array of signature strings for secrets.
7
+ *
8
+ * @param {string[]} signatures - Array of extracted signature strings
9
+ * @param {string} filePath - Source file path (used in redaction message)
10
+ * @returns {{ safe: string[], redacted: boolean }}
11
+ * safe — signatures with any secret-containing entries replaced
12
+ * redacted — true if at least one signature was redacted
13
+ */
14
+ function scan(signatures, filePath) {
15
+ if (!Array.isArray(signatures)) return { safe: [], redacted: false };
16
+
17
+ try {
18
+ let redacted = false;
19
+ const safe = signatures.map((sig) => {
20
+ if (typeof sig !== 'string') return sig;
21
+ for (const pattern of PATTERNS) {
22
+ if (pattern.regex.test(sig)) {
23
+ redacted = true;
24
+ return `[REDACTED — ${pattern.name} detected in ${filePath}]`;
25
+ }
26
+ }
27
+ return sig;
28
+ });
29
+ return { safe, redacted };
30
+ } catch (_) {
31
+ // Never throw — return original signatures on any error
32
+ return { safe: signatures, redacted: false };
33
+ }
34
+ }
35
+
36
+ module.exports = { scan };
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * SigMap usage logger (v0.9)
5
+ *
6
+ * Writes an append-only newline-delimited JSON (NDJSON) log at
7
+ * .context/usage.ndjson
8
+ *
9
+ * Each line is one JSON object describing a gen-context run.
10
+ * Zero npm dependencies — pure Node.js fs.
11
+ *
12
+ * Enabled by:
13
+ * config.tracking: true (gen-context.config.json)
14
+ * --track CLI flag
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const LOG_FILE = path.join('.context', 'usage.ndjson');
21
+
22
+ /**
23
+ * Append one run entry to the usage log.
24
+ * @param {object} entry - Run metrics from runGenerate()
25
+ * @param {string} cwd - Project root (absolute path)
26
+ */
27
+ function logRun(entry, cwd) {
28
+ try {
29
+ const logPath = path.join(cwd, LOG_FILE);
30
+ const dir = path.dirname(logPath);
31
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
32
+
33
+ const record = {
34
+ ts: new Date().toISOString(),
35
+ version: entry.version || '0.9.0',
36
+ fileCount: entry.fileCount || 0,
37
+ droppedCount: entry.droppedCount || 0,
38
+ rawTokens: entry.rawTokens || 0,
39
+ finalTokens: entry.finalTokens || 0,
40
+ reductionPct: entry.rawTokens > 0
41
+ ? parseFloat((100 - (entry.finalTokens / entry.rawTokens) * 100).toFixed(1))
42
+ : 0,
43
+ overBudget: entry.overBudget || false,
44
+ budgetLimit: entry.budgetLimit || 6000,
45
+ };
46
+
47
+ fs.appendFileSync(logPath, JSON.stringify(record) + '\n', 'utf8');
48
+ } catch (err) {
49
+ // Never crash the main process — tracking is optional
50
+ process.stderr.write(`[sigmap] tracking: could not write log: ${err.message}\n`);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Read and parse all usage log entries.
56
+ * @param {string} cwd - Project root (absolute path)
57
+ * @returns {object[]} Array of parsed log records (oldest first)
58
+ */
59
+ function readLog(cwd) {
60
+ try {
61
+ const logPath = path.join(cwd, LOG_FILE);
62
+ if (!fs.existsSync(logPath)) return [];
63
+ const raw = fs.readFileSync(logPath, 'utf8');
64
+ return raw
65
+ .split('\n')
66
+ .filter(Boolean)
67
+ .map((line) => {
68
+ try { return JSON.parse(line); } catch (_) { return null; }
69
+ })
70
+ .filter(Boolean);
71
+ } catch (_) {
72
+ return [];
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Compute summary statistics from an array of log records.
78
+ * @param {object[]} entries
79
+ * @returns {object} Summary stats
80
+ */
81
+ function summarize(entries) {
82
+ if (!entries || entries.length === 0) {
83
+ return {
84
+ totalRuns: 0,
85
+ avgReductionPct: 0,
86
+ avgFinalTokens: 0,
87
+ avgRawTokens: 0,
88
+ minFinalTokens: 0,
89
+ maxFinalTokens: 0,
90
+ firstRun: null,
91
+ lastRun: null,
92
+ overBudgetRuns: 0,
93
+ };
94
+ }
95
+
96
+ const reductions = entries.map((e) => e.reductionPct || 0);
97
+ const finals = entries.map((e) => e.finalTokens || 0);
98
+ const raws = entries.map((e) => e.rawTokens || 0);
99
+
100
+ const avg = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
101
+
102
+ return {
103
+ totalRuns: entries.length,
104
+ avgReductionPct: parseFloat(avg(reductions).toFixed(1)),
105
+ avgFinalTokens: Math.round(avg(finals)),
106
+ avgRawTokens: Math.round(avg(raws)),
107
+ minFinalTokens: Math.min(...finals),
108
+ maxFinalTokens: Math.max(...finals),
109
+ firstRun: entries[0].ts || null,
110
+ lastRun: entries[entries.length - 1].ts || null,
111
+ overBudgetRuns: entries.filter((e) => e.overBudget).length,
112
+ };
113
+ }
114
+
115
+ module.exports = { logRun, readLog, summarize };