sigmap 6.14.0 → 7.0.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.
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Canonical "how to use SigMap" guidance block (v6.16/v7.0).
5
+ *
6
+ * Every adapter emits this one identical block so all generated context files
7
+ * (CLAUDE.md, AGENTS.md, .github/copilot-instructions.md, GEMINI.md, .cursorrules,
8
+ * …) carry the same, single usage section — instead of each adapter inventing
9
+ * its own wording (and codex emitting a redundant second JSON block).
10
+ */
11
+
12
+ function usageBlock() {
13
+ return [
14
+ '## SigMap commands',
15
+ '',
16
+ '| When | Command |',
17
+ '|------|---------|',
18
+ '| Before answering a question about code | `sigmap ask "<your question>"` |',
19
+ '| To rank files by topic | `sigmap --query "<topic>"` |',
20
+ '| After changing config or source dirs | `sigmap validate` |',
21
+ '| To verify an AI answer is grounded | `sigmap judge --response <file>` |',
22
+ '',
23
+ 'Always run `sigmap ask` (or `sigmap --query`) before searching for files relevant to a task.',
24
+ '',
25
+ ].join('\n');
26
+ }
27
+
28
+ module.exports = { usageBlock };
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Hallucination Guard report view (Surface A, v6.15.0).
5
+ *
6
+ * Turns the `verify-ai-output --json` result into a standalone, self-contained
7
+ * HTML report — red/amber/green per issue, with closest-match suggestions
8
+ * inline. The visual language deliberately mirrors the planned PR-comment
9
+ * styling so a single screenshot is reusable across docs and CI (plan proof #5).
10
+ *
11
+ * Zero dependencies, inline CSS/SVG, no external assets. Also exports a compact
12
+ * Markdown renderer for CI / PR comments that shares the same structure.
13
+ */
14
+
15
+ const TYPE_META = {
16
+ 'fake-file': { label: 'Fake file', tone: 'red', icon: '✕' },
17
+ 'fake-test-file': { label: 'Fake test file', tone: 'red', icon: '✕' },
18
+ 'fake-import': { label: 'Fake import', tone: 'red', icon: '✕' },
19
+ 'fake-npm-script': { label: 'Fake npm script', tone: 'red', icon: '✕' },
20
+ 'fake-symbol': { label: 'Fake symbol', tone: 'amber', icon: '!' },
21
+ };
22
+
23
+ function escapeHtml(value) {
24
+ return String(value == null ? '' : value)
25
+ .replace(/&/g, '&amp;')
26
+ .replace(/</g, '&lt;')
27
+ .replace(/>/g, '&gt;')
28
+ .replace(/"/g, '&quot;');
29
+ }
30
+
31
+ function toneFor(issue) {
32
+ const meta = TYPE_META[issue.type];
33
+ if (meta) return meta.tone;
34
+ return issue.confidence === 'high' ? 'red' : 'amber';
35
+ }
36
+
37
+ function labelFor(issue) {
38
+ return (TYPE_META[issue.type] && TYPE_META[issue.type].label) || issue.type;
39
+ }
40
+
41
+ /**
42
+ * Render the verify result to a full HTML document.
43
+ * @param {{ file?: string, issues: object[], summary: object }} result
44
+ * @param {object} [opts]
45
+ * @param {string} [opts.title]
46
+ * @returns {string} HTML
47
+ */
48
+ function renderReportHtml(result, opts = {}) {
49
+ const issues = Array.isArray(result.issues) ? result.issues : [];
50
+ const summary = result.summary || { total: issues.length, byType: {}, clean: issues.length === 0 };
51
+ const file = result.file || opts.file || 'AI answer';
52
+ const title = opts.title || 'SigMap — Hallucination Guard report';
53
+ const clean = summary.clean || issues.length === 0;
54
+ const byType = summary.byType || {};
55
+
56
+ const chips = Object.keys(TYPE_META)
57
+ .filter((t) => byType[t])
58
+ .map((t) => `<span class="chip chip-${TYPE_META[t].tone}">${escapeHtml(TYPE_META[t].label)}: ${byType[t]}</span>`)
59
+ .join('');
60
+
61
+ const banner = clean
62
+ ? `<div class="banner banner-green"><span class="dot"></span> No hallucinations detected — ${escapeHtml(String(summary.symbolsIndexed || 0))} symbols indexed</div>`
63
+ : `<div class="banner banner-red"><span class="dot"></span> ${issues.length} issue${issues.length === 1 ? '' : 's'} found in <code>${escapeHtml(file)}</code></div>`;
64
+
65
+ const rows = issues.map((issue) => {
66
+ const tone = toneFor(issue);
67
+ const sugg = issue.suggestion
68
+ ? `<div class="suggestion">↳ ${escapeHtml(issue.suggestion)} <span class="conf">heuristic</span></div>`
69
+ : '';
70
+ return [
71
+ `<li class="issue issue-${tone}">`,
72
+ ` <div class="issue-head">`,
73
+ ` <span class="badge badge-${tone}">${escapeHtml(labelFor(issue))}</span>`,
74
+ ` <span class="loc">${escapeHtml(issue.location || ('L' + issue.line))}</span>`,
75
+ ` <span class="conf conf-${escapeHtml(issue.confidence || 'high')}">${escapeHtml(issue.confidence || 'high')} confidence</span>`,
76
+ ` </div>`,
77
+ ` <div class="msg">${escapeHtml(issue.message || issue.value)}</div>`,
78
+ ` ${sugg}`,
79
+ `</li>`,
80
+ ].join('\n');
81
+ }).join('\n');
82
+
83
+ const list = clean
84
+ ? '<p class="empty">Nothing to report — every file, import, symbol, and script in the answer resolves against the repository.</p>'
85
+ : `<ul class="issues">${rows}</ul>`;
86
+
87
+ return `<!DOCTYPE html>
88
+ <html lang="en">
89
+ <head>
90
+ <meta charset="utf-8">
91
+ <meta name="viewport" content="width=device-width, initial-scale=1">
92
+ <title>${escapeHtml(title)}</title>
93
+ <style>
94
+ :root { color-scheme: light dark; }
95
+ * { box-sizing: border-box; }
96
+ body { font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background: #0d1117; color: #e6edf3; }
97
+ .wrap { max-width: 880px; margin: 0 auto; padding: 32px 20px 64px; }
98
+ h1 { font-size: 20px; margin: 0 0 4px; }
99
+ .sub { color: #8b949e; margin: 0 0 20px; font-size: 13px; }
100
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: #161b22; padding: 1px 5px; border-radius: 4px; font-size: 12.5px; }
101
+ .banner { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-radius: 8px; font-weight: 600; margin-bottom: 16px; }
102
+ .banner-green { background: rgba(46,160,67,.15); border: 1px solid rgba(46,160,67,.4); color: #3fb950; }
103
+ .banner-red { background: rgba(248,81,73,.12); border: 1px solid rgba(248,81,73,.4); color: #f85149; }
104
+ .dot { width: 9px; height: 9px; border-radius: 50%; background: currentColor; }
105
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 20px; }
106
+ .chip { font-size: 12px; padding: 3px 9px; border-radius: 999px; font-weight: 600; }
107
+ .chip-red { background: rgba(248,81,73,.15); color: #f85149; }
108
+ .chip-amber { background: rgba(210,153,34,.18); color: #d29922; }
109
+ ul.issues { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
110
+ .issue { border: 1px solid #30363d; border-left-width: 4px; border-radius: 8px; padding: 12px 14px; background: #0f141a; }
111
+ .issue-red { border-left-color: #f85149; }
112
+ .issue-amber { border-left-color: #d29922; }
113
+ .issue-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
114
+ .badge { font-size: 11.5px; font-weight: 700; padding: 2px 8px; border-radius: 5px; text-transform: uppercase; letter-spacing: .03em; }
115
+ .badge-red { background: rgba(248,81,73,.18); color: #f85149; }
116
+ .badge-amber { background: rgba(210,153,34,.2); color: #d29922; }
117
+ .loc { font-family: ui-monospace, monospace; color: #8b949e; font-size: 12px; }
118
+ .conf { font-size: 11px; color: #8b949e; }
119
+ .conf-high { color: #f85149; }
120
+ .conf-medium { color: #d29922; }
121
+ .msg { margin-top: 7px; font-family: ui-monospace, monospace; font-size: 12.5px; color: #e6edf3; }
122
+ .suggestion { margin-top: 6px; font-size: 12.5px; color: #3fb950; }
123
+ .suggestion .conf { margin-left: 6px; }
124
+ .empty { color: #8b949e; }
125
+ footer { margin-top: 28px; color: #6e7681; font-size: 12px; }
126
+ a { color: #58a6ff; }
127
+ </style>
128
+ </head>
129
+ <body>
130
+ <div class="wrap">
131
+ <h1>Hallucination Guard report</h1>
132
+ <p class="sub">Deterministic verification of an AI answer against the real repository — <code>sigmap verify-ai-output</code></p>
133
+ ${banner}
134
+ ${chips ? `<div class="chips">${chips}</div>` : ''}
135
+ ${list}
136
+ <footer>Generated by <a href="https://github.com/manojmallick/sigmap">SigMap</a> · offline, no LLM · suggestions are heuristic (closest-match)</footer>
137
+ </div>
138
+ </body>
139
+ </html>
140
+ `;
141
+ }
142
+
143
+ /** Compact Markdown rendering of the same result (CI / PR comments). */
144
+ function renderReportMarkdown(result) {
145
+ const issues = Array.isArray(result.issues) ? result.issues : [];
146
+ const summary = result.summary || {};
147
+ const file = result.file || 'AI answer';
148
+ if (summary.clean || issues.length === 0) {
149
+ return `### ✅ Hallucination Guard — clean\n\nNo fabricated files, imports, symbols, or scripts in \`${file}\`.`;
150
+ }
151
+ const lines = [
152
+ `### ❌ Hallucination Guard — ${issues.length} issue${issues.length === 1 ? '' : 's'} in \`${file}\``,
153
+ '',
154
+ '| Type | Location | Detail | Suggestion |',
155
+ '| --- | --- | --- | --- |',
156
+ ];
157
+ for (const i of issues) {
158
+ const sugg = i.suggestion ? i.suggestion.replace(/\|/g, '\\|') : '—';
159
+ lines.push(`| ${labelFor(i)} | ${i.location || ('L' + i.line)} | \`${String(i.value).replace(/\|/g, '\\|')}\` | ${sugg} |`);
160
+ }
161
+ return lines.join('\n');
162
+ }
163
+
164
+ module.exports = { renderReportHtml, renderReportMarkdown, escapeHtml };
@@ -505,4 +505,47 @@ function getLines(args, cwd) {
505
505
  ].join('\n');
506
506
  }
507
507
 
508
- module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines };
508
+ /**
509
+ * read_memory({ limit? }) → string
510
+ *
511
+ * Recall the cross-session decision log (notes) plus the last ranking-session
512
+ * focus, formatted for an agent to consume at the start of a task.
513
+ */
514
+ function readMemory(args, cwd) {
515
+ let limit = parseInt(args && args.limit, 10);
516
+ if (!Number.isFinite(limit) || limit <= 0) limit = 10;
517
+ limit = Math.min(limit, 50);
518
+
519
+ const out = ['# SigMap memory'];
520
+
521
+ let notes = [];
522
+ try {
523
+ const { readNotes, formatNotes } = require('../session/notes');
524
+ notes = readNotes(cwd, limit);
525
+ out.push('');
526
+ out.push(`## Recent notes (${notes.length})`);
527
+ // Most recent first for quick scanning.
528
+ out.push(formatNotes(notes.slice().reverse()));
529
+ } catch (_) {
530
+ out.push('');
531
+ out.push('_No notes available._');
532
+ }
533
+
534
+ // Last ranking-session focus (if any) — extends src/session/memory.js.
535
+ try {
536
+ const { loadSession } = require('../session/memory');
537
+ const s = loadSession(cwd);
538
+ if (s && (s.lastQuery || (s.topFiles && s.topFiles.length))) {
539
+ out.push('');
540
+ out.push('## Last session');
541
+ if (s.lastQuery) out.push(`**Last query:** ${s.lastQuery}`);
542
+ if (s.topFiles && s.topFiles.length) {
543
+ out.push(`**Focus files:** ${s.topFiles.map((f) => f.file).slice(0, 5).join(', ')}`);
544
+ }
545
+ }
546
+ } catch (_) { /* session optional */ }
547
+
548
+ return out.join('\n');
549
+ }
550
+
551
+ module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory };
package/src/mcp/server.js CHANGED
@@ -8,17 +8,17 @@
8
8
  *
9
9
  * Supported methods:
10
10
  * initialize → serverInfo + capabilities
11
- * tools/list → 10 tool definitions
11
+ * tools/list → 11 tool definitions
12
12
  * tools/call → dispatch to handler, return result
13
13
  */
14
14
 
15
15
  const readline = require('readline');
16
16
  const { TOOLS } = require('./tools');
17
- const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines } = require('./handlers');
17
+ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory } = require('./handlers');
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '6.14.0',
21
+ version: '7.0.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -76,6 +76,7 @@ function dispatch(msg, cwd) {
76
76
  else if (name === 'query_context') text = queryContext(args, cwd);
77
77
  else if (name === 'get_impact') text = getImpact(args, cwd);
78
78
  else if (name === 'get_lines') text = getLines(args, cwd);
79
+ else if (name === 'read_memory') text = readMemory(args, cwd);
79
80
  else {
80
81
  respondError(id, -32601, `Unknown tool: ${name}`);
81
82
  return;
package/src/mcp/tools.js CHANGED
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * MCP tool definitions for SigMap (10 tools).
4
+ * MCP tool definitions for SigMap (11 tools).
5
5
  * read_context, search_signatures, get_map, create_checkpoint, get_routing,
6
- * explain_file, list_modules, query_context, get_impact, get_lines.
6
+ * explain_file, list_modules, query_context, get_impact, get_lines, read_memory.
7
7
  */
8
8
 
9
9
  const TOOLS = [
@@ -197,6 +197,24 @@ const TOOLS = [
197
197
  required: ['file', 'start', 'end'],
198
198
  },
199
199
  },
200
+ {
201
+ name: 'read_memory',
202
+ description:
203
+ 'Recall the project decision log — recent notes left by humans or agents ' +
204
+ 'across sessions (via `sigmap note`), plus the last ranking-session focus. ' +
205
+ 'Call this at the start of a task to kill cold-start: it answers ' +
206
+ '"what were we doing and why" without re-reading the whole codebase.',
207
+ inputSchema: {
208
+ type: 'object',
209
+ properties: {
210
+ limit: {
211
+ type: 'number',
212
+ description: 'How many of the most recent notes to return (default: 10, max: 50).',
213
+ },
214
+ },
215
+ required: [],
216
+ },
217
+ },
200
218
  ];
201
219
 
202
220
  module.exports = { TOOLS };
package/src/nudge.js ADDED
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Star nudge + usage tracking (v7.0.0).
5
+ *
6
+ * Records run counts in `.context/usage.json` and shows a one-time GitHub-star
7
+ * message after the tool has been genuinely useful (≥10 runs, ≥8 successes).
8
+ * Shown exactly once per machine — even under concurrent runs (an `wx` lock
9
+ * file makes the show race-safe). Wired into `ask` (and the `squeeze` path).
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const crypto = require('crypto');
16
+
17
+ const RUN_THRESHOLD = 10;
18
+ const SUCCESS_THRESHOLD = 8;
19
+
20
+ function usagePath(cwd) { return path.join(cwd, '.context', 'usage.json'); }
21
+
22
+ function defaultUsage() {
23
+ return {
24
+ totalRuns: 0, successfulRuns: 0, squeezeOffered: 0, squeezeAccepted: 0,
25
+ starNudgeShown: false, machineId: '', firstRunDate: null, lastRunDate: null,
26
+ };
27
+ }
28
+
29
+ function readUsage(cwd) {
30
+ try { return { ...defaultUsage(), ...JSON.parse(fs.readFileSync(usagePath(cwd), 'utf8')) }; }
31
+ catch (_) { return defaultUsage(); }
32
+ }
33
+
34
+ function writeUsageAtomic(cwd, usage) {
35
+ const p = usagePath(cwd);
36
+ fs.mkdirSync(path.dirname(p), { recursive: true });
37
+ const tmp = `${p}.${process.pid}.tmp`;
38
+ fs.writeFileSync(tmp, JSON.stringify(usage, null, 2));
39
+ fs.renameSync(tmp, p); // atomic on POSIX
40
+ }
41
+
42
+ const STAR_MESSAGE = [
43
+ '─────────────────────────────────────────────────────────',
44
+ ' SigMap has helped you 10 times now.',
45
+ '',
46
+ " If it's been useful, a GitHub star takes 5 seconds and",
47
+ ' helps other developers find it:',
48
+ ' → github.com/manojmallick/sigmap',
49
+ '',
50
+ " (Won't ask again. Press Enter to continue.)",
51
+ '─────────────────────────────────────────────────────────',
52
+ ].join('\n');
53
+
54
+ function showStarNudge(write) {
55
+ (write || ((s) => process.stderr.write(s)))('\n' + STAR_MESSAGE + '\n');
56
+ }
57
+
58
+ /**
59
+ * Record one run and, when the thresholds are first met, show the star nudge.
60
+ * @param {string} cwd
61
+ * @param {boolean} runSuccess
62
+ * @param {object} [opts]
63
+ * @param {boolean} [opts.silent] record only — never print
64
+ * @param {function} [opts.write] sink for the message (default stderr)
65
+ * @param {string} [opts.today] override date (testing)
66
+ * @param {object} [opts.bump] counter deltas to merge (e.g. { squeezeAccepted: 1 })
67
+ * @returns {{ usage, nudged }}
68
+ */
69
+ function checkStarNudge(cwd, runSuccess, opts = {}) {
70
+ const usage = readUsage(cwd);
71
+ usage.totalRuns += 1;
72
+ if (runSuccess) usage.successfulRuns += 1;
73
+ if (opts.bump) for (const k of Object.keys(opts.bump)) usage[k] = (usage[k] || 0) + opts.bump[k];
74
+
75
+ const today = opts.today || new Date().toISOString().slice(0, 10);
76
+ if (!usage.firstRunDate) usage.firstRunDate = today;
77
+ usage.lastRunDate = today;
78
+ if (!usage.machineId) {
79
+ try { usage.machineId = 'sha256-' + crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 16); } catch (_) {}
80
+ }
81
+
82
+ let nudged = false;
83
+ if (!usage.starNudgeShown && usage.totalRuns >= RUN_THRESHOLD && usage.successfulRuns >= SUCCESS_THRESHOLD) {
84
+ // Race-safe single-show: only the process that creates the lock prints.
85
+ let won = false;
86
+ try { fs.closeSync(fs.openSync(usagePath(cwd) + '.nudge.lock', 'wx')); won = true; }
87
+ catch (_) { won = false; }
88
+ if (won && !opts.silent) showStarNudge(opts.write);
89
+ nudged = won;
90
+ usage.starNudgeShown = true;
91
+ }
92
+
93
+ writeUsageAtomic(cwd, usage);
94
+ return { usage, nudged };
95
+ }
96
+
97
+ module.exports = { checkStarNudge, readUsage, usagePath, showStarNudge, RUN_THRESHOLD, SUCCESS_THRESHOLD };
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Decision-log notes (Memory, v6.15.0).
5
+ *
6
+ * A tiny append-only log of human/agent notes that survives across sessions,
7
+ * so an agent starting cold can recall "what were we doing / why". Stored as
8
+ * NDJSON under `.context/` to match the project's other state logs
9
+ * (usage.ndjson, benchmark-history.ndjson). Zero dependencies.
10
+ *
11
+ * Consumed by the `read_memory` MCP tool and the `sigmap note` / `sigmap status`
12
+ * commands. Complements `src/session/memory.js` (ranking session) rather than
13
+ * duplicating it.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { execSync } = require('child_process');
19
+
20
+ const NOTES_FILE = path.join('.context', 'notes.ndjson');
21
+ const MAX_TEXT = 2000;
22
+
23
+ function notesPath(cwd) {
24
+ return path.join(cwd, NOTES_FILE);
25
+ }
26
+
27
+ function _currentBranch(cwd) {
28
+ try {
29
+ return execSync('git rev-parse --abbrev-ref HEAD', {
30
+ cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
31
+ }).trim() || null;
32
+ } catch (_) {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Append a note. Returns the stored entry.
39
+ * @param {string} cwd
40
+ * @param {string} text
41
+ * @param {object} [opts]
42
+ * @param {string|null} [opts.branch] override branch (default: current git branch)
43
+ * @param {string} [opts.tag] optional category tag
44
+ */
45
+ function addNote(cwd, text, opts = {}) {
46
+ const clean = String(text == null ? '' : text).trim().slice(0, MAX_TEXT);
47
+ if (!clean) throw new Error('note text is empty');
48
+ const entry = {
49
+ ts: new Date().toISOString(),
50
+ text: clean,
51
+ branch: opts.branch !== undefined ? opts.branch : _currentBranch(cwd),
52
+ };
53
+ if (opts.tag) entry.tag = String(opts.tag).trim().slice(0, 40);
54
+ const p = notesPath(cwd);
55
+ fs.mkdirSync(path.dirname(p), { recursive: true });
56
+ fs.appendFileSync(p, JSON.stringify(entry) + '\n');
57
+ return entry;
58
+ }
59
+
60
+ /**
61
+ * Read notes in chronological order (oldest first).
62
+ * @param {string} cwd
63
+ * @param {number} [limit=0] keep only the most recent N (0 = all)
64
+ * @returns {object[]}
65
+ */
66
+ function readNotes(cwd, limit = 0) {
67
+ let raw;
68
+ try { raw = fs.readFileSync(notesPath(cwd), 'utf8'); }
69
+ catch (_) { return []; }
70
+ const out = [];
71
+ for (const line of raw.split('\n')) {
72
+ const t = line.trim();
73
+ if (!t) continue;
74
+ try { out.push(JSON.parse(t)); } catch (_) { /* skip corrupt line */ }
75
+ }
76
+ if (limit > 0 && out.length > limit) return out.slice(out.length - limit);
77
+ return out;
78
+ }
79
+
80
+ /** Format notes as a Markdown list (pass already-ordered notes). */
81
+ function formatNotes(notes) {
82
+ if (!notes || !notes.length) {
83
+ return 'No notes recorded yet. Add one with: sigmap note "<text>"';
84
+ }
85
+ return notes.map((n) => {
86
+ const when = String(n.ts || '').replace('T', ' ').slice(0, 16);
87
+ const br = n.branch ? ` (${n.branch})` : '';
88
+ const tag = n.tag ? ` #${n.tag}` : '';
89
+ return `- [${when}${br}]${tag} ${n.text}`;
90
+ }).join('\n');
91
+ }
92
+
93
+ /** Delete the notes log. Returns true if a file was removed. */
94
+ function clearNotes(cwd) {
95
+ try { fs.unlinkSync(notesPath(cwd)); return true; }
96
+ catch (_) { return false; }
97
+ }
98
+
99
+ module.exports = { notesPath, addNote, readNotes, formatNotes, clearNotes };
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CI / build-log squeeze (v7.0.0).
5
+ *
6
+ * Strips timestamps, progress bars, and repeated noise; keeps every error line
7
+ * plus a small context window around it. Never returns empty — when there are
8
+ * no errors it falls back to a head/tail summary. Also reused by the stacktrace
9
+ * squeezer to clean noise surrounding a trace.
10
+ */
11
+
12
+ const TS_PREFIX_RE = /^\s*(?:\[?\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?Z?\]?\s*|\[?\d{1,2}:\d{2}:\d{2}(?:[.,]\d+)?\]?\s*)+/;
13
+ const PROGRESS_LINE_RE = /(?:^|\s)(?:\d{1,3}%|Downloading|Receiving objects|Resolving deltas|Compressing objects|npm (?:WARN|notice|http|sill|verb)|##\[(?:group|endgroup|command|section)\]|\[\d+\/\d+\]|ETA[: ]|█|━|▕|⣿)/;
14
+ const ERROR_RE = /\b(?:error|err!|fail(?:ed|ure)?|exception|panic|fatal|traceback|ERR_|E[A-Z]{3,})\b|✗|❌/i;
15
+
16
+ /** Remove a leading timestamp prefix from a single line. */
17
+ function stripTimestamp(line) {
18
+ return line.replace(TS_PREFIX_RE, '');
19
+ }
20
+
21
+ /**
22
+ * @param {string} input
23
+ * @param {object} [opts]
24
+ * @param {number} [opts.context=2] context lines kept around each error
25
+ * @returns {{ squeezed: string, kept: string[], stripped: string[] }}
26
+ */
27
+ function squeezeCiLog(input, opts = {}) {
28
+ const ctx = opts.context != null ? opts.context : 2;
29
+ const lines = input.split('\n');
30
+ const keep = new Set();
31
+ const errorIdx = [];
32
+
33
+ for (let i = 0; i < lines.length; i++) {
34
+ if (ERROR_RE.test(lines[i])) {
35
+ errorIdx.push(i);
36
+ for (let j = Math.max(0, i - ctx); j <= Math.min(lines.length - 1, i + ctx); j++) keep.add(j);
37
+ }
38
+ }
39
+
40
+ let body;
41
+ let keptDesc;
42
+ if (errorIdx.length === 0) {
43
+ const head = lines.slice(0, 10).map(stripTimestamp);
44
+ const tail = lines.length > 20 ? lines.slice(-10).map(stripTimestamp) : [];
45
+ body = head.slice();
46
+ if (tail.length) body.push(`… (${lines.length - 20} lines omitted) …`, ...tail);
47
+ keptDesc = ['head/tail summary (no error lines found)'];
48
+ } else {
49
+ const sorted = [...keep].sort((a, b) => a - b);
50
+ body = [];
51
+ let prev = -2;
52
+ for (const idx of sorted) {
53
+ // Drop pure progress/noise lines unless they are themselves errors.
54
+ const line = stripTimestamp(lines[idx]);
55
+ if (PROGRESS_LINE_RE.test(line) && !ERROR_RE.test(line)) continue;
56
+ if (idx > prev + 1) body.push(`… (${idx - prev - 1} lines) …`);
57
+ body.push(line);
58
+ prev = idx;
59
+ }
60
+ if (body.length === 0) body = errorIdx.map((i) => stripTimestamp(lines[i])); // safety net
61
+ keptDesc = [`${errorIdx.length} error line(s) + ${ctx}-line context`];
62
+ }
63
+
64
+ return {
65
+ squeezed: body.join('\n'),
66
+ kept: keptDesc,
67
+ stripped: [`${Math.max(0, lines.length - body.length)} timestamp/progress/noise line(s)`],
68
+ };
69
+ }
70
+
71
+ module.exports = { squeezeCiLog, stripTimestamp, ERROR_RE };