sigmap 8.23.0 → 8.25.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/CHANGELOG.md +24 -0
- package/README.md +4 -4
- package/gen-context.js +345 -2
- package/llms-full.txt +6 -3
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/config/tune.js +210 -0
- package/src/mcp/server.js +1 -1
- package/src/security/redact.js +56 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,30 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.25.0] — 2026-08-18
|
|
14
|
+
|
|
15
|
+
Minor release — **"Agent Economy II" (v8.25, F2)**: the discovery stack becomes a config optimizer — one command that recommends (and can apply) the config a repo actually needs.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`sigmap tune` (#514, PR #515):** new `src/config/tune.js` — `buildTuneProposal(cwd)` packages the existing discovery stack (`resolveSourceRoots`, workspace markers, client-artifact probes) into a deterministic recommended-config diff with one evidence-naming reason per change. Five rules, explicit user choices never overridden: **srcDirs pin** (resolver roots, confidence-gated, never proposed against user-pinned dirs — pinned srcDirs are stable across runs and protected from token-budget drops), **monorepo** (fires when a workspace marker exists and the mode is off; reason names the marker — `pnpm-workspace.yaml`/`turbo.json`/`nx.json`/`lerna.json`/package.json `workspaces`), **adapters** (additive from client artifacts present: `CLAUDE.md`→claude, `.cursorrules`/`.cursor/`→cursor, `.windsurfrules`→windsurf, `AGENTS.md`→codex), **exclude** (curated vendored/generated dirs present at root — `third_party`, `external(s)`, `generated`, `testdata`, `tmp`, `.cache` … — appended with defaults preserved), **autoMaxTokens** (fires only when a pinned budget is below a labeled ~25-tokens/file heuristic estimate). `applyTuneProposal` merges into `gen-context.config.json` preserving every user key; re-proposal after apply is empty (idempotent). CLI: `sigmap tune` is read-only by default (`--dry-run` alias), `--apply` writes and points to `sigmap validate`, `--json` for agents. Zero new dependencies.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- 10 new integration tests (131 test files); bundle rebuilt (148 modules).
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## [8.24.0] — 2026-07-28
|
|
26
|
+
|
|
27
|
+
Minor release — **"Trust Quick Wins I" (v8.24, G3a)**: the secret-redaction engine that already protects signatures, `get_lines`, and evidence packs becomes a standalone command for arbitrary text.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- **`sigmap redact` (#511, PR #512):** new `src/security/redact.js` — `redactText()` masks only the matched secret substring (`[REDACTED:<pattern name>]`), preserving surrounding text (unlike the generation-time scanner's whole-line replacement, which is unchanged). Findings carry 1-based line numbers with per-pattern counts. CLI: `sigmap redact [file] [--json]` — file argument or stdin; redacted text on stdout (pipe-clean), summary on stderr. Reuses the existing 10-pattern bank verbatim; zero new dependencies; never throws. 6 new integration tests including an every-pattern mask sweep (130 test files).
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- Test fixtures for secret patterns are assembled at runtime (no secret-shaped literals in committed blobs) — GitHub Push Protection rejected the first push over the fake samples, which validated the detection class this command implements.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
13
37
|
## [8.23.0] — 2026-07-28
|
|
14
38
|
|
|
15
39
|
Minor release — **"Agent Economy I" (v8.23, F1)**: SigMap's token savings become queryable *during* a session. A spend ledger over the existing gain log, an optional budget threshold, and context-freshness age — as a CLI command and the 21st MCP tool.
|
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ That map is exactly what agentic grep is worst at: reproducible, auditable conte
|
|
|
59
59
|
|
|
60
60
|
**Proof it pays off** (full benchmark below):
|
|
61
61
|
<!--SM:whyMetrics-->
|
|
62
|
-
- **82.2% hit@5** — right file in top 5 results (vs 44.
|
|
62
|
+
- **82.2% hit@5** — right file in top 5 results (vs 44.0% single-shot grep baseline — 1.87× lift)
|
|
63
63
|
- **96.8% token reduction** — average across 21 real repos
|
|
64
64
|
- **64.8% task-success proxy** — modeled from retrieval tiers, not measured LLM sessions
|
|
65
65
|
- **1.53 prompts per task** — down from 2.84 (46.1% fewer retries, modeled)
|
|
@@ -122,10 +122,10 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
122
122
|
|
|
123
123
|
<!--SM:benchmarkBlock-->
|
|
124
124
|
```
|
|
125
|
-
Benchmark : sigmap-v8.
|
|
126
|
-
Date : 2026-
|
|
125
|
+
Benchmark : sigmap-v8.25-main (21 repositories, including R language)
|
|
126
|
+
Date : 2026-08-17
|
|
127
127
|
|
|
128
|
-
Hit@5 : 82.2% (grep-agent baseline 44.
|
|
128
|
+
Hit@5 : 82.2% (grep-agent baseline 44.0% — 1.87× lift)
|
|
129
129
|
Token reduction: 96.8% (across 21 repos)
|
|
130
130
|
Prompt reduction : 46.1% (2.84 → 1.53 prompts per task, modeled)
|
|
131
131
|
Task success : 64.8% (proxy — modeled from retrieval tiers)
|
package/gen-context.js
CHANGED
|
@@ -1877,6 +1877,220 @@ __factories["./src/config/loader"] = function(module, exports) {
|
|
|
1877
1877
|
|
|
1878
1878
|
};
|
|
1879
1879
|
|
|
1880
|
+
// ── ./src/config/tune ──
|
|
1881
|
+
__factories["./src/config/tune"] = function(module, exports) {
|
|
1882
|
+
|
|
1883
|
+
/**
|
|
1884
|
+
* sigmap tune — deterministic config optimizer (F2, #514).
|
|
1885
|
+
*
|
|
1886
|
+
* Packages the existing discovery stack (source-root-resolver, monorepo
|
|
1887
|
+
* markers, client-artifact probes) into a recommended config diff with a
|
|
1888
|
+
* one-line reason per change. Read-only by default; `applyTuneProposal`
|
|
1889
|
+
* merges accepted changes into gen-context.config.json, preserving every
|
|
1890
|
+
* user key. Explicit user choices are never proposed against.
|
|
1891
|
+
*/
|
|
1892
|
+
|
|
1893
|
+
const fs = require('fs');
|
|
1894
|
+
const path = require('path');
|
|
1895
|
+
const { loadConfig } = __require('./src/config/loader');
|
|
1896
|
+
const { resolveSourceRoots } = __require('./src/discovery/source-root-resolver');
|
|
1897
|
+
|
|
1898
|
+
// Workspace markers, in probe order (reason names the first one found).
|
|
1899
|
+
const MONOREPO_MARKERS = ['pnpm-workspace.yaml', 'turbo.json', 'nx.json', 'lerna.json'];
|
|
1900
|
+
|
|
1901
|
+
// Client artifacts → adapter names (additive only).
|
|
1902
|
+
const ADAPTER_MARKERS = [
|
|
1903
|
+
{ adapter: 'claude', files: ['CLAUDE.md'] },
|
|
1904
|
+
{ adapter: 'cursor', files: ['.cursorrules', '.cursor'] },
|
|
1905
|
+
{ adapter: 'windsurf', files: ['.windsurfrules', '.windsurf'] },
|
|
1906
|
+
{ adapter: 'codex', files: ['AGENTS.md'] },
|
|
1907
|
+
];
|
|
1908
|
+
|
|
1909
|
+
// Root-level dirs that are typically vendored/generated when present.
|
|
1910
|
+
const JUNK_DIRS = [
|
|
1911
|
+
'third_party', 'thirdparty', 'external', 'externals',
|
|
1912
|
+
'generated', 'testdata', 'snapshots', 'tmp', 'temp', '.cache',
|
|
1913
|
+
];
|
|
1914
|
+
|
|
1915
|
+
// Rough signature cost per source file (chars/4 world) for the budget check.
|
|
1916
|
+
const TOKENS_PER_FILE = 25;
|
|
1917
|
+
|
|
1918
|
+
const SOURCE_EXTS = new Set([
|
|
1919
|
+
'.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.py', '.rb', '.go', '.rs',
|
|
1920
|
+
'.java', '.kt', '.cs', '.cpp', '.c', '.h', '.hpp', '.swift', '.dart',
|
|
1921
|
+
'.scala', '.php', '.gd', '.r', '.R',
|
|
1922
|
+
]);
|
|
1923
|
+
|
|
1924
|
+
/** Raw user config file content, or null when absent/unparsable. */
|
|
1925
|
+
function _readUserConfig(cwd) {
|
|
1926
|
+
try {
|
|
1927
|
+
return JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
|
|
1928
|
+
} catch (_) {
|
|
1929
|
+
return null;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
/** Count source files under `roots` (relative to cwd), depth-capped, deterministic. */
|
|
1934
|
+
function _countSourceFiles(cwd, roots, exclude, depth = 5) {
|
|
1935
|
+
const excSet = new Set(exclude || []);
|
|
1936
|
+
let count = 0;
|
|
1937
|
+
const walk = (dir, d) => {
|
|
1938
|
+
if (d <= 0 || count > 20000) return;
|
|
1939
|
+
let entries;
|
|
1940
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
1941
|
+
for (const e of entries) {
|
|
1942
|
+
if (excSet.has(e.name)) continue;
|
|
1943
|
+
if (e.isDirectory()) walk(path.join(dir, e.name), d - 1);
|
|
1944
|
+
else if (e.isFile() && SOURCE_EXTS.has(path.extname(e.name))) count++;
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
for (const r of roots) {
|
|
1948
|
+
const full = path.join(cwd, r);
|
|
1949
|
+
if (fs.existsSync(full)) walk(full, depth);
|
|
1950
|
+
}
|
|
1951
|
+
return count;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
/** The workspace marker present at cwd, or null. */
|
|
1955
|
+
function _monorepoMarker(cwd) {
|
|
1956
|
+
for (const m of MONOREPO_MARKERS) {
|
|
1957
|
+
if (fs.existsSync(path.join(cwd, m))) return m;
|
|
1958
|
+
}
|
|
1959
|
+
try {
|
|
1960
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
1961
|
+
if (pkg.workspaces) return 'package.json workspaces';
|
|
1962
|
+
} catch (_) {}
|
|
1963
|
+
return null;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
/**
|
|
1967
|
+
* Build the recommended config diff for a repo.
|
|
1968
|
+
*
|
|
1969
|
+
* @param {string} cwd
|
|
1970
|
+
* @returns {{ changes: Array<{key:string, current:*, recommended:*, reason:string}>,
|
|
1971
|
+
* detection: { roots:string[], confidence:string, isMonorepo:boolean },
|
|
1972
|
+
* configExists: boolean }}
|
|
1973
|
+
*/
|
|
1974
|
+
function buildTuneProposal(cwd) {
|
|
1975
|
+
const userConfig = _readUserConfig(cwd);
|
|
1976
|
+
const config = loadConfig(cwd);
|
|
1977
|
+
const detection = resolveSourceRoots(cwd, { exclude: config.exclude });
|
|
1978
|
+
const changes = [];
|
|
1979
|
+
|
|
1980
|
+
// 1. srcDirs — recommend pinning the detected roots when the user hasn't.
|
|
1981
|
+
// Pinned srcDirs make generation explicit/stable and are protected from
|
|
1982
|
+
// token-budget drops; user-pinned srcDirs are never proposed against.
|
|
1983
|
+
const userPinnedSrcDirs = !!(userConfig && Array.isArray(userConfig.srcDirs));
|
|
1984
|
+
if (!userPinnedSrcDirs && detection.roots.length > 0 && detection.confidence !== 'low') {
|
|
1985
|
+
changes.push({
|
|
1986
|
+
key: 'srcDirs',
|
|
1987
|
+
current: null,
|
|
1988
|
+
recommended: detection.roots,
|
|
1989
|
+
reason: `pin the ${detection.roots.length} detected source root(s) [confidence ${detection.confidence}] — explicit srcDirs are stable across runs and protected from budget drops`,
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// 2. monorepo — a workspace marker exists but the mode is off.
|
|
1994
|
+
const marker = _monorepoMarker(cwd);
|
|
1995
|
+
if (marker && config.monorepo !== true) {
|
|
1996
|
+
changes.push({
|
|
1997
|
+
key: 'monorepo',
|
|
1998
|
+
current: config.monorepo,
|
|
1999
|
+
recommended: true,
|
|
2000
|
+
reason: `workspace marker found: ${marker}`,
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// 3. adapters — client artifacts present that the adapter list doesn't cover.
|
|
2005
|
+
const currentAdapters = Array.isArray(config.adapters) ? config.adapters
|
|
2006
|
+
: (Array.isArray(config.outputs) ? config.outputs : ['copilot']);
|
|
2007
|
+
const found = [];
|
|
2008
|
+
for (const { adapter, files } of ADAPTER_MARKERS) {
|
|
2009
|
+
if (currentAdapters.includes(adapter)) continue;
|
|
2010
|
+
const hit = files.find((f) => fs.existsSync(path.join(cwd, f)));
|
|
2011
|
+
if (hit) found.push({ adapter, hit });
|
|
2012
|
+
}
|
|
2013
|
+
if (found.length > 0) {
|
|
2014
|
+
changes.push({
|
|
2015
|
+
key: 'adapters',
|
|
2016
|
+
current: currentAdapters,
|
|
2017
|
+
recommended: [...currentAdapters, ...found.map((f) => f.adapter)],
|
|
2018
|
+
reason: `client files present: ${found.map((f) => f.hit).join(', ')}`,
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// 4. exclude — root-level vendored/generated dirs not excluded yet.
|
|
2023
|
+
const junkFound = JUNK_DIRS.filter((d) => {
|
|
2024
|
+
if ((config.exclude || []).includes(d)) return false;
|
|
2025
|
+
try { return fs.statSync(path.join(cwd, d)).isDirectory(); } catch (_) { return false; }
|
|
2026
|
+
});
|
|
2027
|
+
if (junkFound.length > 0) {
|
|
2028
|
+
changes.push({
|
|
2029
|
+
key: 'exclude',
|
|
2030
|
+
current: userConfig && userConfig.exclude ? userConfig.exclude : null,
|
|
2031
|
+
recommended: [...((userConfig && userConfig.exclude) || config.exclude), ...junkFound],
|
|
2032
|
+
reason: `present at root and typically vendored/generated: ${junkFound.join(', ')}`,
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// 5. autoMaxTokens — a pinned budget that the repo's size will overflow.
|
|
2037
|
+
if (config.autoMaxTokens === false) {
|
|
2038
|
+
const roots = detection.roots.length > 0 ? detection.roots : config.srcDirs;
|
|
2039
|
+
const files = _countSourceFiles(cwd, roots, config.exclude);
|
|
2040
|
+
const estTokens = files * TOKENS_PER_FILE;
|
|
2041
|
+
if (estTokens > config.maxTokens) {
|
|
2042
|
+
changes.push({
|
|
2043
|
+
key: 'autoMaxTokens',
|
|
2044
|
+
current: false,
|
|
2045
|
+
recommended: true,
|
|
2046
|
+
reason: `~${files} source files need ~${estTokens} tokens (heuristic) but maxTokens is pinned at ${config.maxTokens} — auto-scaling targets ${Math.round((config.coverageTarget || 0.8) * 100)}% coverage`,
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
return {
|
|
2052
|
+
changes,
|
|
2053
|
+
detection: { roots: detection.roots, confidence: detection.confidence, isMonorepo: detection.isMonorepo },
|
|
2054
|
+
configExists: userConfig !== null,
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
/**
|
|
2059
|
+
* Merge a proposal's changes into gen-context.config.json (create if absent).
|
|
2060
|
+
* Preserves every existing user key; only the proposed keys are written.
|
|
2061
|
+
*
|
|
2062
|
+
* @returns {{ path: string, applied: string[] }}
|
|
2063
|
+
*/
|
|
2064
|
+
function applyTuneProposal(cwd, proposal) {
|
|
2065
|
+
const cfgPath = path.join(cwd, 'gen-context.config.json');
|
|
2066
|
+
const existing = _readUserConfig(cwd) || {};
|
|
2067
|
+
for (const c of proposal.changes) existing[c.key] = c.recommended;
|
|
2068
|
+
fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n');
|
|
2069
|
+
return { path: cfgPath, applied: proposal.changes.map((c) => c.key) };
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
/** Human rendering of a proposal (one block per change, reason indented). */
|
|
2073
|
+
function formatTuneProposal(proposal) {
|
|
2074
|
+
const lines = [];
|
|
2075
|
+
if (proposal.changes.length === 0) {
|
|
2076
|
+
lines.push('[sigmap] tune: config already matches detection — no changes recommended');
|
|
2077
|
+
} else {
|
|
2078
|
+
lines.push(`[sigmap] tune: ${proposal.changes.length} recommended change(s)`);
|
|
2079
|
+
for (const c of proposal.changes) {
|
|
2080
|
+
lines.push(` ${c.key.padEnd(14)} ${JSON.stringify(c.current)} → ${JSON.stringify(c.recommended)}`);
|
|
2081
|
+
lines.push(` ${''.padEnd(14)} reason: ${c.reason}`);
|
|
2082
|
+
}
|
|
2083
|
+
lines.push('');
|
|
2084
|
+
lines.push(' apply with: sigmap tune --apply (then: sigmap validate)');
|
|
2085
|
+
}
|
|
2086
|
+
lines.push(` detection: roots [${proposal.detection.roots.join(', ')}] · confidence ${proposal.detection.confidence} · monorepo ${proposal.detection.isMonorepo ? 'yes' : 'no'}`);
|
|
2087
|
+
return lines.join('\n');
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
module.exports = { buildTuneProposal, applyTuneProposal, formatTuneProposal, JUNK_DIRS, TOKENS_PER_FILE };
|
|
2091
|
+
|
|
2092
|
+
};
|
|
2093
|
+
|
|
1880
2094
|
// ── ./src/conventions/ci ──
|
|
1881
2095
|
__factories["./src/conventions/ci"] = function(module, exports) {
|
|
1882
2096
|
|
|
@@ -14981,7 +15195,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
14981
15195
|
|
|
14982
15196
|
const SERVER_INFO = {
|
|
14983
15197
|
name: 'sigmap',
|
|
14984
|
-
version: '8.
|
|
15198
|
+
version: '8.25.0',
|
|
14985
15199
|
description: 'SigMap MCP server — code signatures on demand',
|
|
14986
15200
|
};
|
|
14987
15201
|
|
|
@@ -17528,6 +17742,66 @@ __factories["./src/security/patterns"] = function(module, exports) {
|
|
|
17528
17742
|
|
|
17529
17743
|
};
|
|
17530
17744
|
|
|
17745
|
+
// ── ./src/security/redact ──
|
|
17746
|
+
__factories["./src/security/redact"] = function(module, exports) {
|
|
17747
|
+
|
|
17748
|
+
/**
|
|
17749
|
+
* Standalone text redaction (v8.24 G3a) — the same pattern bank the
|
|
17750
|
+
* generation-time scanner uses (security/patterns.js), applied to arbitrary
|
|
17751
|
+
* text. Unlike scanner.js (which replaces whole signature lines), this masks
|
|
17752
|
+
* only the matched secret substring so surrounding text stays readable.
|
|
17753
|
+
*
|
|
17754
|
+
* Zero dependencies; never throws — on any error the original text is
|
|
17755
|
+
* returned unredacted.
|
|
17756
|
+
*/
|
|
17757
|
+
|
|
17758
|
+
const { PATTERNS } = __require('./src/security/patterns');
|
|
17759
|
+
|
|
17760
|
+
// Global variants of the pattern regexes (needed for replace-all per line).
|
|
17761
|
+
const GLOBAL_PATTERNS = PATTERNS.map((p) => ({
|
|
17762
|
+
name: p.name,
|
|
17763
|
+
regex: new RegExp(p.regex.source, p.regex.flags.includes('g') ? p.regex.flags : p.regex.flags + 'g'),
|
|
17764
|
+
}));
|
|
17765
|
+
|
|
17766
|
+
/**
|
|
17767
|
+
* Redact secrets in arbitrary text.
|
|
17768
|
+
* @param {string} text
|
|
17769
|
+
* @returns {{
|
|
17770
|
+
* text: string, redacted: boolean,
|
|
17771
|
+
* findings: Array<{ line: number, pattern: string }>,
|
|
17772
|
+
* counts: Object<string, number>
|
|
17773
|
+
* }}
|
|
17774
|
+
*/
|
|
17775
|
+
function redactText(text) {
|
|
17776
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
17777
|
+
return { text: typeof text === 'string' ? text : '', redacted: false, findings: [], counts: {} };
|
|
17778
|
+
}
|
|
17779
|
+
try {
|
|
17780
|
+
const findings = [];
|
|
17781
|
+
const counts = {};
|
|
17782
|
+
const lines = text.split('\n');
|
|
17783
|
+
const out = lines.map((line, i) => {
|
|
17784
|
+
let masked = line;
|
|
17785
|
+
for (const p of GLOBAL_PATTERNS) {
|
|
17786
|
+
p.regex.lastIndex = 0;
|
|
17787
|
+
if (!p.regex.test(masked)) continue;
|
|
17788
|
+
p.regex.lastIndex = 0;
|
|
17789
|
+
masked = masked.replace(p.regex, `[REDACTED:${p.name}]`);
|
|
17790
|
+
findings.push({ line: i + 1, pattern: p.name });
|
|
17791
|
+
counts[p.name] = (counts[p.name] || 0) + 1;
|
|
17792
|
+
}
|
|
17793
|
+
return masked;
|
|
17794
|
+
});
|
|
17795
|
+
return { text: out.join('\n'), redacted: findings.length > 0, findings, counts };
|
|
17796
|
+
} catch (_) {
|
|
17797
|
+
return { text, redacted: false, findings: [], counts: {} };
|
|
17798
|
+
}
|
|
17799
|
+
}
|
|
17800
|
+
|
|
17801
|
+
module.exports = { redactText };
|
|
17802
|
+
|
|
17803
|
+
};
|
|
17804
|
+
|
|
17531
17805
|
// ── ./src/security/scanner ──
|
|
17532
17806
|
__factories["./src/security/scanner"] = function(module, exports) {
|
|
17533
17807
|
|
|
@@ -20347,7 +20621,7 @@ function __tryGit(args, opts = {}) {
|
|
|
20347
20621
|
catch (_) { return ''; }
|
|
20348
20622
|
}
|
|
20349
20623
|
|
|
20350
|
-
const VERSION = '8.
|
|
20624
|
+
const VERSION = '8.25.0';
|
|
20351
20625
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
20352
20626
|
|
|
20353
20627
|
function requireSourceOrBundled(key) {
|
|
@@ -22234,6 +22508,9 @@ Usage:
|
|
|
22234
22508
|
${cmd} memory --clear <store> Clear one store: session|notes|weights|evidence|all (--json supported)
|
|
22235
22509
|
${cmd} budget Session spend ledger — estimated SigMap-emitted tokens, budget, context age (--json)
|
|
22236
22510
|
${cmd} budget --budget <tokens> One-off budget override (config: sessionBudgetTokens, contextTtlDays)
|
|
22511
|
+
${cmd} redact [file] Mask secrets in a file or stdin (10-pattern bank); redacted text to stdout (--json)
|
|
22512
|
+
${cmd} tune Recommend config from repo detection — srcDirs, monorepo, adapters, exclude, budget (--json)
|
|
22513
|
+
${cmd} tune --apply Write the recommendations into gen-context.config.json (merges; your keys preserved)
|
|
22237
22514
|
${cmd} note "<text>" Append a note to the cross-session decision log
|
|
22238
22515
|
${cmd} note List recent notes (also: note --list <N>)
|
|
22239
22516
|
${cmd} status Show repo state — branch, dirty files, index freshness, notes
|
|
@@ -23633,6 +23910,36 @@ function main() {
|
|
|
23633
23910
|
process.exit(0);
|
|
23634
23911
|
}
|
|
23635
23912
|
|
|
23913
|
+
// `sigmap tune [--apply|--dry-run] [--json]` — deterministic config optimizer:
|
|
23914
|
+
// recommend srcDirs/monorepo/adapters/exclude/autoMaxTokens from the existing
|
|
23915
|
+
// discovery stack, one reason per change. Read-only unless --apply.
|
|
23916
|
+
if (args[0] === 'tune') {
|
|
23917
|
+
const { buildTuneProposal, applyTuneProposal, formatTuneProposal } = requireSourceOrBundled('./src/config/tune');
|
|
23918
|
+
let proposal;
|
|
23919
|
+
try {
|
|
23920
|
+
proposal = buildTuneProposal(cwd);
|
|
23921
|
+
} catch (err) {
|
|
23922
|
+
console.error(`[sigmap] tune failed: ${err.message}`);
|
|
23923
|
+
process.exit(1);
|
|
23924
|
+
}
|
|
23925
|
+
if (args.includes('--apply') && proposal.changes.length > 0) {
|
|
23926
|
+
const res = applyTuneProposal(cwd, proposal);
|
|
23927
|
+
if (args.includes('--json')) {
|
|
23928
|
+
process.stdout.write(JSON.stringify({ ...proposal, applied: res.applied, path: res.path }) + '\n');
|
|
23929
|
+
} else {
|
|
23930
|
+
console.log(formatTuneProposal(proposal));
|
|
23931
|
+
console.log(`[sigmap] tune: wrote ${res.applied.join(', ')} → ${path.relative(cwd, res.path)} — run: sigmap validate`);
|
|
23932
|
+
}
|
|
23933
|
+
process.exit(0);
|
|
23934
|
+
}
|
|
23935
|
+
if (args.includes('--json')) {
|
|
23936
|
+
process.stdout.write(JSON.stringify(proposal) + '\n');
|
|
23937
|
+
} else {
|
|
23938
|
+
console.log(formatTuneProposal(proposal));
|
|
23939
|
+
}
|
|
23940
|
+
process.exit(0);
|
|
23941
|
+
}
|
|
23942
|
+
|
|
23636
23943
|
// `sigmap note "<text>"` — append to the cross-session decision log.
|
|
23637
23944
|
// With no text, lists recent notes (also `note --list [N]`).
|
|
23638
23945
|
// `sigmap memory` — one view over the cross-session stores in .context/.
|
|
@@ -23715,6 +24022,42 @@ function main() {
|
|
|
23715
24022
|
process.exit(0);
|
|
23716
24023
|
}
|
|
23717
24024
|
|
|
24025
|
+
if (args[0] === 'redact') {
|
|
24026
|
+
const jsonOut = args.includes('--json');
|
|
24027
|
+
const { redactText } = requireSourceOrBundled('./src/security/redact');
|
|
24028
|
+
const fileArg = args.slice(1).find((a) => !a.startsWith('--'));
|
|
24029
|
+
let input;
|
|
24030
|
+
if (fileArg) {
|
|
24031
|
+
try {
|
|
24032
|
+
input = fs.readFileSync(path.resolve(cwd, fileArg), 'utf8');
|
|
24033
|
+
} catch (err) {
|
|
24034
|
+
console.error(`[sigmap] redact: cannot read ${fileArg}: ${err.message}`);
|
|
24035
|
+
process.exit(1);
|
|
24036
|
+
}
|
|
24037
|
+
} else if (!process.stdin.isTTY) {
|
|
24038
|
+
try {
|
|
24039
|
+
input = fs.readFileSync(0, 'utf8');
|
|
24040
|
+
} catch (err) {
|
|
24041
|
+
console.error(`[sigmap] redact: cannot read stdin: ${err.message}`);
|
|
24042
|
+
process.exit(1);
|
|
24043
|
+
}
|
|
24044
|
+
} else {
|
|
24045
|
+
console.error('[sigmap] usage: sigmap redact <file> or <cmd> | sigmap redact');
|
|
24046
|
+
process.exit(1);
|
|
24047
|
+
}
|
|
24048
|
+
const r = redactText(input);
|
|
24049
|
+
if (jsonOut) {
|
|
24050
|
+
process.stdout.write(JSON.stringify(r) + '\n');
|
|
24051
|
+
process.exit(0);
|
|
24052
|
+
}
|
|
24053
|
+
process.stdout.write(r.text);
|
|
24054
|
+
const parts = Object.entries(r.counts).map(([k, v]) => `${k}×${v}`);
|
|
24055
|
+
console.error(r.redacted
|
|
24056
|
+
? `[sigmap] redact: masked ${r.findings.length} secret(s) — ${parts.join(', ')}`
|
|
24057
|
+
: '[sigmap] redact: no secrets found');
|
|
24058
|
+
process.exit(0);
|
|
24059
|
+
}
|
|
24060
|
+
|
|
23718
24061
|
if (args[0] === 'note') {
|
|
23719
24062
|
const jsonOut = args.includes('--json');
|
|
23720
24063
|
const { addNote, readNotes, formatNotes } = requireSourceOrBundled('./src/session/notes');
|
package/llms-full.txt
CHANGED
|
@@ -11,17 +11,17 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.25.0 | Benchmark: sigmap-v8.25-main (2026-08-17)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
20
|
+
## Core metrics (benchmark: sigmap-v8.25-main, 2026-08-17)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
|
24
|
-
| Retrieval hit@5 | 44.
|
|
24
|
+
| Retrieval hit@5 | 44.0% (single-shot grep) | 82.2% (1.87× lift) |
|
|
25
25
|
| Token reduction | — | 96.8% average |
|
|
26
26
|
| Task-success proxy (modeled) | — | 64.8% |
|
|
27
27
|
| Prompts per task | 2.84 | 1.53 (46.1% fewer) |
|
|
@@ -127,6 +127,9 @@ sigmap memory List cross-session stores (.context/)
|
|
|
127
127
|
sigmap memory --clear <store> Clear one store: session|notes|weights|evidence|all (--json supported)
|
|
128
128
|
sigmap budget Session spend ledger — estimated SigMap-emitted tokens, budget, context age (--json)
|
|
129
129
|
sigmap budget --budget <tokens> One-off budget override (config: sessionBudgetTokens, contextTtlDays)
|
|
130
|
+
sigmap redact [file] Mask secrets in a file or stdin (10-pattern bank); redacted text to stdout (--json)
|
|
131
|
+
sigmap tune Recommend config from repo detection — srcDirs, monorepo, adapters, exclude, budget (--json)
|
|
132
|
+
sigmap tune --apply Write the recommendations into gen-context.config.json (merges; your keys preserved)
|
|
130
133
|
sigmap note "<text>" Append a note to the cross-session decision log
|
|
131
134
|
sigmap note List recent notes (also: note --list <N>)
|
|
132
135
|
sigmap status Show repo state — branch, dirty files, index freshness, notes
|
package/llms.txt
CHANGED
|
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.25.0 | Benchmark: sigmap-v8.25-main (2026-08-17)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
@@ -23,9 +23,9 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
23
23
|
- No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
|
|
24
24
|
- Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
|
|
25
25
|
|
|
26
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
26
|
+
## Core metrics (benchmark: sigmap-v8.25-main, 2026-08-17)
|
|
27
27
|
|
|
28
|
-
- hit@5 retrieval: 82.2% vs 44.
|
|
28
|
+
- hit@5 retrieval: 82.2% vs 44.0% single-shot grep baseline (1.87× lift)
|
|
29
29
|
- Token reduction: 96.8% average across benchmark repos
|
|
30
30
|
- Task-success proxy: 64.8% (modeled from retrieval tiers, not measured LLM sessions)
|
|
31
31
|
- Prompts per task: 1.53 vs 2.84 baseline (46.1% fewer, modeled)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.25.0",
|
|
4
4
|
"description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sigmap tune — deterministic config optimizer (F2, #514).
|
|
5
|
+
*
|
|
6
|
+
* Packages the existing discovery stack (source-root-resolver, monorepo
|
|
7
|
+
* markers, client-artifact probes) into a recommended config diff with a
|
|
8
|
+
* one-line reason per change. Read-only by default; `applyTuneProposal`
|
|
9
|
+
* merges accepted changes into gen-context.config.json, preserving every
|
|
10
|
+
* user key. Explicit user choices are never proposed against.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { loadConfig } = require('./loader');
|
|
16
|
+
const { resolveSourceRoots } = require('../discovery/source-root-resolver');
|
|
17
|
+
|
|
18
|
+
// Workspace markers, in probe order (reason names the first one found).
|
|
19
|
+
const MONOREPO_MARKERS = ['pnpm-workspace.yaml', 'turbo.json', 'nx.json', 'lerna.json'];
|
|
20
|
+
|
|
21
|
+
// Client artifacts → adapter names (additive only).
|
|
22
|
+
const ADAPTER_MARKERS = [
|
|
23
|
+
{ adapter: 'claude', files: ['CLAUDE.md'] },
|
|
24
|
+
{ adapter: 'cursor', files: ['.cursorrules', '.cursor'] },
|
|
25
|
+
{ adapter: 'windsurf', files: ['.windsurfrules', '.windsurf'] },
|
|
26
|
+
{ adapter: 'codex', files: ['AGENTS.md'] },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Root-level dirs that are typically vendored/generated when present.
|
|
30
|
+
const JUNK_DIRS = [
|
|
31
|
+
'third_party', 'thirdparty', 'external', 'externals',
|
|
32
|
+
'generated', 'testdata', 'snapshots', 'tmp', 'temp', '.cache',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Rough signature cost per source file (chars/4 world) for the budget check.
|
|
36
|
+
const TOKENS_PER_FILE = 25;
|
|
37
|
+
|
|
38
|
+
const SOURCE_EXTS = new Set([
|
|
39
|
+
'.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.py', '.rb', '.go', '.rs',
|
|
40
|
+
'.java', '.kt', '.cs', '.cpp', '.c', '.h', '.hpp', '.swift', '.dart',
|
|
41
|
+
'.scala', '.php', '.gd', '.r', '.R',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/** Raw user config file content, or null when absent/unparsable. */
|
|
45
|
+
function _readUserConfig(cwd) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
|
|
48
|
+
} catch (_) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Count source files under `roots` (relative to cwd), depth-capped, deterministic. */
|
|
54
|
+
function _countSourceFiles(cwd, roots, exclude, depth = 5) {
|
|
55
|
+
const excSet = new Set(exclude || []);
|
|
56
|
+
let count = 0;
|
|
57
|
+
const walk = (dir, d) => {
|
|
58
|
+
if (d <= 0 || count > 20000) return;
|
|
59
|
+
let entries;
|
|
60
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
61
|
+
for (const e of entries) {
|
|
62
|
+
if (excSet.has(e.name)) continue;
|
|
63
|
+
if (e.isDirectory()) walk(path.join(dir, e.name), d - 1);
|
|
64
|
+
else if (e.isFile() && SOURCE_EXTS.has(path.extname(e.name))) count++;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
for (const r of roots) {
|
|
68
|
+
const full = path.join(cwd, r);
|
|
69
|
+
if (fs.existsSync(full)) walk(full, depth);
|
|
70
|
+
}
|
|
71
|
+
return count;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The workspace marker present at cwd, or null. */
|
|
75
|
+
function _monorepoMarker(cwd) {
|
|
76
|
+
for (const m of MONOREPO_MARKERS) {
|
|
77
|
+
if (fs.existsSync(path.join(cwd, m))) return m;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
81
|
+
if (pkg.workspaces) return 'package.json workspaces';
|
|
82
|
+
} catch (_) {}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build the recommended config diff for a repo.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} cwd
|
|
90
|
+
* @returns {{ changes: Array<{key:string, current:*, recommended:*, reason:string}>,
|
|
91
|
+
* detection: { roots:string[], confidence:string, isMonorepo:boolean },
|
|
92
|
+
* configExists: boolean }}
|
|
93
|
+
*/
|
|
94
|
+
function buildTuneProposal(cwd) {
|
|
95
|
+
const userConfig = _readUserConfig(cwd);
|
|
96
|
+
const config = loadConfig(cwd);
|
|
97
|
+
const detection = resolveSourceRoots(cwd, { exclude: config.exclude });
|
|
98
|
+
const changes = [];
|
|
99
|
+
|
|
100
|
+
// 1. srcDirs — recommend pinning the detected roots when the user hasn't.
|
|
101
|
+
// Pinned srcDirs make generation explicit/stable and are protected from
|
|
102
|
+
// token-budget drops; user-pinned srcDirs are never proposed against.
|
|
103
|
+
const userPinnedSrcDirs = !!(userConfig && Array.isArray(userConfig.srcDirs));
|
|
104
|
+
if (!userPinnedSrcDirs && detection.roots.length > 0 && detection.confidence !== 'low') {
|
|
105
|
+
changes.push({
|
|
106
|
+
key: 'srcDirs',
|
|
107
|
+
current: null,
|
|
108
|
+
recommended: detection.roots,
|
|
109
|
+
reason: `pin the ${detection.roots.length} detected source root(s) [confidence ${detection.confidence}] — explicit srcDirs are stable across runs and protected from budget drops`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. monorepo — a workspace marker exists but the mode is off.
|
|
114
|
+
const marker = _monorepoMarker(cwd);
|
|
115
|
+
if (marker && config.monorepo !== true) {
|
|
116
|
+
changes.push({
|
|
117
|
+
key: 'monorepo',
|
|
118
|
+
current: config.monorepo,
|
|
119
|
+
recommended: true,
|
|
120
|
+
reason: `workspace marker found: ${marker}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 3. adapters — client artifacts present that the adapter list doesn't cover.
|
|
125
|
+
const currentAdapters = Array.isArray(config.adapters) ? config.adapters
|
|
126
|
+
: (Array.isArray(config.outputs) ? config.outputs : ['copilot']);
|
|
127
|
+
const found = [];
|
|
128
|
+
for (const { adapter, files } of ADAPTER_MARKERS) {
|
|
129
|
+
if (currentAdapters.includes(adapter)) continue;
|
|
130
|
+
const hit = files.find((f) => fs.existsSync(path.join(cwd, f)));
|
|
131
|
+
if (hit) found.push({ adapter, hit });
|
|
132
|
+
}
|
|
133
|
+
if (found.length > 0) {
|
|
134
|
+
changes.push({
|
|
135
|
+
key: 'adapters',
|
|
136
|
+
current: currentAdapters,
|
|
137
|
+
recommended: [...currentAdapters, ...found.map((f) => f.adapter)],
|
|
138
|
+
reason: `client files present: ${found.map((f) => f.hit).join(', ')}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 4. exclude — root-level vendored/generated dirs not excluded yet.
|
|
143
|
+
const junkFound = JUNK_DIRS.filter((d) => {
|
|
144
|
+
if ((config.exclude || []).includes(d)) return false;
|
|
145
|
+
try { return fs.statSync(path.join(cwd, d)).isDirectory(); } catch (_) { return false; }
|
|
146
|
+
});
|
|
147
|
+
if (junkFound.length > 0) {
|
|
148
|
+
changes.push({
|
|
149
|
+
key: 'exclude',
|
|
150
|
+
current: userConfig && userConfig.exclude ? userConfig.exclude : null,
|
|
151
|
+
recommended: [...((userConfig && userConfig.exclude) || config.exclude), ...junkFound],
|
|
152
|
+
reason: `present at root and typically vendored/generated: ${junkFound.join(', ')}`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 5. autoMaxTokens — a pinned budget that the repo's size will overflow.
|
|
157
|
+
if (config.autoMaxTokens === false) {
|
|
158
|
+
const roots = detection.roots.length > 0 ? detection.roots : config.srcDirs;
|
|
159
|
+
const files = _countSourceFiles(cwd, roots, config.exclude);
|
|
160
|
+
const estTokens = files * TOKENS_PER_FILE;
|
|
161
|
+
if (estTokens > config.maxTokens) {
|
|
162
|
+
changes.push({
|
|
163
|
+
key: 'autoMaxTokens',
|
|
164
|
+
current: false,
|
|
165
|
+
recommended: true,
|
|
166
|
+
reason: `~${files} source files need ~${estTokens} tokens (heuristic) but maxTokens is pinned at ${config.maxTokens} — auto-scaling targets ${Math.round((config.coverageTarget || 0.8) * 100)}% coverage`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
changes,
|
|
173
|
+
detection: { roots: detection.roots, confidence: detection.confidence, isMonorepo: detection.isMonorepo },
|
|
174
|
+
configExists: userConfig !== null,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Merge a proposal's changes into gen-context.config.json (create if absent).
|
|
180
|
+
* Preserves every existing user key; only the proposed keys are written.
|
|
181
|
+
*
|
|
182
|
+
* @returns {{ path: string, applied: string[] }}
|
|
183
|
+
*/
|
|
184
|
+
function applyTuneProposal(cwd, proposal) {
|
|
185
|
+
const cfgPath = path.join(cwd, 'gen-context.config.json');
|
|
186
|
+
const existing = _readUserConfig(cwd) || {};
|
|
187
|
+
for (const c of proposal.changes) existing[c.key] = c.recommended;
|
|
188
|
+
fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n');
|
|
189
|
+
return { path: cfgPath, applied: proposal.changes.map((c) => c.key) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Human rendering of a proposal (one block per change, reason indented). */
|
|
193
|
+
function formatTuneProposal(proposal) {
|
|
194
|
+
const lines = [];
|
|
195
|
+
if (proposal.changes.length === 0) {
|
|
196
|
+
lines.push('[sigmap] tune: config already matches detection — no changes recommended');
|
|
197
|
+
} else {
|
|
198
|
+
lines.push(`[sigmap] tune: ${proposal.changes.length} recommended change(s)`);
|
|
199
|
+
for (const c of proposal.changes) {
|
|
200
|
+
lines.push(` ${c.key.padEnd(14)} ${JSON.stringify(c.current)} → ${JSON.stringify(c.recommended)}`);
|
|
201
|
+
lines.push(` ${''.padEnd(14)} reason: ${c.reason}`);
|
|
202
|
+
}
|
|
203
|
+
lines.push('');
|
|
204
|
+
lines.push(' apply with: sigmap tune --apply (then: sigmap validate)');
|
|
205
|
+
}
|
|
206
|
+
lines.push(` detection: roots [${proposal.detection.roots.join(', ')}] · confidence ${proposal.detection.confidence} · monorepo ${proposal.detection.isMonorepo ? 'yes' : 'no'}`);
|
|
207
|
+
return lines.join('\n');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { buildTuneProposal, applyTuneProposal, formatTuneProposal, JUNK_DIRS, TOKENS_PER_FILE };
|
package/src/mcp/server.js
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standalone text redaction (v8.24 G3a) — the same pattern bank the
|
|
5
|
+
* generation-time scanner uses (security/patterns.js), applied to arbitrary
|
|
6
|
+
* text. Unlike scanner.js (which replaces whole signature lines), this masks
|
|
7
|
+
* only the matched secret substring so surrounding text stays readable.
|
|
8
|
+
*
|
|
9
|
+
* Zero dependencies; never throws — on any error the original text is
|
|
10
|
+
* returned unredacted.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { PATTERNS } = require('./patterns');
|
|
14
|
+
|
|
15
|
+
// Global variants of the pattern regexes (needed for replace-all per line).
|
|
16
|
+
const GLOBAL_PATTERNS = PATTERNS.map((p) => ({
|
|
17
|
+
name: p.name,
|
|
18
|
+
regex: new RegExp(p.regex.source, p.regex.flags.includes('g') ? p.regex.flags : p.regex.flags + 'g'),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Redact secrets in arbitrary text.
|
|
23
|
+
* @param {string} text
|
|
24
|
+
* @returns {{
|
|
25
|
+
* text: string, redacted: boolean,
|
|
26
|
+
* findings: Array<{ line: number, pattern: string }>,
|
|
27
|
+
* counts: Object<string, number>
|
|
28
|
+
* }}
|
|
29
|
+
*/
|
|
30
|
+
function redactText(text) {
|
|
31
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
32
|
+
return { text: typeof text === 'string' ? text : '', redacted: false, findings: [], counts: {} };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const findings = [];
|
|
36
|
+
const counts = {};
|
|
37
|
+
const lines = text.split('\n');
|
|
38
|
+
const out = lines.map((line, i) => {
|
|
39
|
+
let masked = line;
|
|
40
|
+
for (const p of GLOBAL_PATTERNS) {
|
|
41
|
+
p.regex.lastIndex = 0;
|
|
42
|
+
if (!p.regex.test(masked)) continue;
|
|
43
|
+
p.regex.lastIndex = 0;
|
|
44
|
+
masked = masked.replace(p.regex, `[REDACTED:${p.name}]`);
|
|
45
|
+
findings.push({ line: i + 1, pattern: p.name });
|
|
46
|
+
counts[p.name] = (counts[p.name] || 0) + 1;
|
|
47
|
+
}
|
|
48
|
+
return masked;
|
|
49
|
+
});
|
|
50
|
+
return { text: out.join('\n'), redacted: findings.length > 0, findings, counts };
|
|
51
|
+
} catch (_) {
|
|
52
|
+
return { text, redacted: false, findings: [], counts: {} };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { redactText };
|