sigmap 6.11.1 → 6.13.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 +37 -0
- package/README.md +3 -3
- package/gen-context.js +449 -88
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/extractors/javascript.js +44 -7
- package/src/extractors/typescript.js +22 -6
- package/src/format/dashboard.js +105 -0
- package/src/mcp/handlers.js +58 -1
- package/src/mcp/server.js +4 -3
- package/src/mcp/tools.js +31 -2
package/gen-context.js
CHANGED
|
@@ -916,61 +916,92 @@ __factories["./src/extractors/java"] = function(module, exports) {
|
|
|
916
916
|
|
|
917
917
|
// ── ./src/extractors/javascript ──
|
|
918
918
|
__factories["./src/extractors/javascript"] = function(module, exports) {
|
|
919
|
-
|
|
919
|
+
|
|
920
|
+
const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
|
|
921
|
+
|
|
920
922
|
/**
|
|
921
923
|
* Extract signatures from JavaScript source code.
|
|
924
|
+
* Top-level declarations and class members carry a `:start-end` line anchor
|
|
925
|
+
* (see line-anchor.js); kept parallel to `sigs` and applied once at return.
|
|
922
926
|
* @param {string} src - Raw file content
|
|
923
927
|
* @returns {string[]} Array of signature strings
|
|
924
928
|
*/
|
|
925
929
|
function extract(src) {
|
|
926
930
|
if (!src || typeof src !== 'string') return [];
|
|
927
931
|
const sigs = [];
|
|
932
|
+
const anchors = [];
|
|
928
933
|
const returnHints = buildReturnHints(src);
|
|
929
|
-
|
|
934
|
+
|
|
935
|
+
// Block comments are blanked newline-by-newline (non-newline chars → spaces)
|
|
936
|
+
// so character offsets AND line numbers stay exact for anchors.
|
|
930
937
|
const stripped = src
|
|
931
938
|
.replace(/\/\/.*$/gm, '')
|
|
932
|
-
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
933
|
-
|
|
939
|
+
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
|
|
940
|
+
|
|
941
|
+
const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;
|
|
942
|
+
// End line for a function whose match ends at `matchEnd` (before its body brace).
|
|
943
|
+
const fnEndLine = (matchEnd, startLn) => {
|
|
944
|
+
const brace = stripped.indexOf('{', matchEnd);
|
|
945
|
+
return brace !== -1 ? lineAt(stripped, blockEndIdx(brace + 1)) : startLn;
|
|
946
|
+
};
|
|
947
|
+
|
|
934
948
|
// Classes
|
|
935
949
|
const classRegex = /^(export\s+(?:default\s+)?)?class\s+(\w+)(?:\s+extends\s+[\w.]+)?\s*\{/gm;
|
|
936
950
|
for (const m of stripped.matchAll(classRegex)) {
|
|
937
951
|
const prefix = m[1] ? m[1].trim() + ' ' : '';
|
|
952
|
+
const bodyStart = m.index + m[0].length;
|
|
938
953
|
sigs.push(`${prefix}class ${m[2]}`);
|
|
939
|
-
|
|
940
|
-
|
|
954
|
+
anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
|
|
955
|
+
const block = extractBlock(stripped, bodyStart);
|
|
956
|
+
for (const meth of extractClassMembers(block, returnHints)) {
|
|
957
|
+
sigs.push(` ${meth.text}`);
|
|
958
|
+
anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
|
|
959
|
+
}
|
|
941
960
|
}
|
|
942
|
-
|
|
961
|
+
|
|
943
962
|
// Exported named functions
|
|
944
963
|
for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
|
|
945
964
|
const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
|
|
946
965
|
const retStr = formatReturnHint(returnHints.get(m[1]));
|
|
966
|
+
const startLn = lineAt(stripped, m.index);
|
|
947
967
|
sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
|
|
968
|
+
anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
|
|
948
969
|
}
|
|
949
|
-
|
|
970
|
+
|
|
950
971
|
// Exported arrow functions
|
|
951
972
|
for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/gm)) {
|
|
952
973
|
const asyncKw = m[0].includes('async') ? 'async ' : '';
|
|
953
974
|
const retStr = formatReturnHint(returnHints.get(m[1]));
|
|
975
|
+
const startLn = lineAt(stripped, m.index);
|
|
954
976
|
sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>${retStr}`);
|
|
977
|
+
anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
|
|
955
978
|
}
|
|
956
|
-
|
|
979
|
+
|
|
957
980
|
// module.exports = { ... }
|
|
958
981
|
const moduleExports = stripped.match(/^module\.exports\s*=\s*\{([^}]+)\}/m);
|
|
959
982
|
if (moduleExports) {
|
|
960
983
|
const names = moduleExports[1].split(',').map((s) => s.trim()).filter(Boolean);
|
|
961
|
-
if (names.length > 0)
|
|
984
|
+
if (names.length > 0) {
|
|
985
|
+
const startLn = lineAt(stripped, moduleExports.index);
|
|
986
|
+
sigs.push(`module.exports = { ${names.join(', ')} }`);
|
|
987
|
+
anchors.push([startLn, lineAt(stripped, moduleExports.index + moduleExports[0].length)]);
|
|
988
|
+
}
|
|
962
989
|
}
|
|
963
|
-
|
|
990
|
+
|
|
964
991
|
// Top-level named functions (non-exported)
|
|
965
992
|
for (const m of stripped.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
|
|
966
993
|
const asyncKw = m[0].startsWith('async') ? 'async ' : '';
|
|
967
994
|
const retStr = formatReturnHint(returnHints.get(m[1]));
|
|
995
|
+
const startLn = lineAt(stripped, m.index);
|
|
968
996
|
sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
|
|
997
|
+
anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
|
|
969
998
|
}
|
|
970
|
-
|
|
971
|
-
return sigs
|
|
999
|
+
|
|
1000
|
+
return sigs
|
|
1001
|
+
.map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s))
|
|
1002
|
+
.slice(0, 25);
|
|
972
1003
|
}
|
|
973
|
-
|
|
1004
|
+
|
|
974
1005
|
function extractBlock(src, startIndex) {
|
|
975
1006
|
let depth = 1;
|
|
976
1007
|
let i = startIndex;
|
|
@@ -982,16 +1013,22 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
|
|
|
982
1013
|
}
|
|
983
1014
|
return src.slice(startIndex, i - 1);
|
|
984
1015
|
}
|
|
985
|
-
|
|
1016
|
+
|
|
1017
|
+
// Returns members as { text, start, end } where start/end are char offsets
|
|
1018
|
+
// WITHIN `block` (end = the method's closing brace), so the caller can resolve
|
|
1019
|
+
// per-method line anchors that span the method body.
|
|
986
1020
|
function extractClassMembers(block, returnHints) {
|
|
987
1021
|
const members = [];
|
|
988
1022
|
for (const m of block.matchAll(/^\s+(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(([^)]*)\)\s*\{/gm)) {
|
|
989
1023
|
if (/^_/.test(m[1])) continue;
|
|
990
|
-
|
|
1024
|
+
const bodyStart = m.index + m[0].length; // just past the opening brace
|
|
1025
|
+
const end = bodyStart + extractBlock(block, bodyStart).length;
|
|
1026
|
+
const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
|
|
1027
|
+
if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
|
|
991
1028
|
const isAsync = m[0].includes('async ') ? 'async ' : '';
|
|
992
1029
|
const isStatic = m[0].includes('static ') ? 'static ' : '';
|
|
993
1030
|
const retStr = formatReturnHint(returnHints.get(m[1]));
|
|
994
|
-
members.push(`${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}
|
|
1031
|
+
members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
|
|
995
1032
|
}
|
|
996
1033
|
return members.slice(0, 8);
|
|
997
1034
|
}
|
|
@@ -1016,18 +1053,17 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
|
|
|
1016
1053
|
}
|
|
1017
1054
|
|
|
1018
1055
|
function formatReturnHint(type) {
|
|
1019
|
-
return type ? `
|
|
1056
|
+
return type ? ` → ${type}` : '';
|
|
1020
1057
|
}
|
|
1021
|
-
|
|
1058
|
+
|
|
1022
1059
|
function normalizeParams(params) {
|
|
1023
1060
|
if (!params) return '';
|
|
1024
1061
|
return params.trim().replace(/\s+/g, ' ');
|
|
1025
1062
|
}
|
|
1026
|
-
|
|
1063
|
+
|
|
1027
1064
|
module.exports = { extract };
|
|
1028
|
-
|
|
1029
|
-
};
|
|
1030
1065
|
|
|
1066
|
+
};
|
|
1031
1067
|
// ── ./src/extractors/kotlin ──
|
|
1032
1068
|
__factories["./src/extractors/kotlin"] = function(module, exports) {
|
|
1033
1069
|
|
|
@@ -1799,101 +1835,164 @@ __factories["./src/extractors/swift"] = function(module, exports) {
|
|
|
1799
1835
|
|
|
1800
1836
|
// ── ./src/extractors/typescript ──
|
|
1801
1837
|
__factories["./src/extractors/typescript"] = function(module, exports) {
|
|
1802
|
-
|
|
1838
|
+
|
|
1839
|
+
const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
|
|
1840
|
+
|
|
1803
1841
|
/**
|
|
1804
1842
|
* Extract signatures from TypeScript source code.
|
|
1843
|
+
* Top-level declarations carry a `:start-end` line anchor (see line-anchor.js);
|
|
1844
|
+
* indented members do not.
|
|
1805
1845
|
* @param {string} src - Raw file content
|
|
1806
1846
|
* @returns {string[]} Array of signature strings
|
|
1807
1847
|
*/
|
|
1808
1848
|
function extract(src) {
|
|
1809
1849
|
if (!src || typeof src !== 'string') return [];
|
|
1810
1850
|
const sigs = [];
|
|
1811
|
-
|
|
1812
|
-
//
|
|
1851
|
+
// anchors[i] is [start, end] for a top-level sig, or null for an indented member.
|
|
1852
|
+
// Kept parallel to `sigs` so existing push/mutation logic stays untouched;
|
|
1853
|
+
// anchors are applied once at return.
|
|
1854
|
+
const anchors = [];
|
|
1855
|
+
|
|
1856
|
+
// Strip comments to simplify matching. Block comments are blanked
|
|
1857
|
+
// newline-by-newline (non-newline chars → spaces) so character offsets AND
|
|
1858
|
+
// line numbers stay exact. Line comments preserve their trailing newline.
|
|
1813
1859
|
const stripped = src
|
|
1814
1860
|
.replace(/\/\/.*$/gm, '')
|
|
1815
|
-
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
1816
|
-
|
|
1861
|
+
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
|
|
1862
|
+
|
|
1863
|
+
// Index of the closing brace for a block whose body starts at bodyStart.
|
|
1864
|
+
const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;
|
|
1865
|
+
|
|
1817
1866
|
// Exported interfaces
|
|
1818
1867
|
for (const m of stripped.matchAll(/^export\s+interface\s+(\w+)(?:<[^{]*>)?\s*(?:extends\s+[^{]+)?\{/gm)) {
|
|
1868
|
+
const bodyStart = m.index + m[0].length;
|
|
1819
1869
|
sigs.push(`export interface ${m[1]}`);
|
|
1870
|
+
anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
|
|
1820
1871
|
// Collect members
|
|
1821
|
-
const
|
|
1822
|
-
const block = extractBlock(stripped, start);
|
|
1872
|
+
const block = extractBlock(stripped, bodyStart);
|
|
1823
1873
|
const members = extractInterfaceMembers(block);
|
|
1824
|
-
for (const mem of members)
|
|
1874
|
+
for (const mem of members) {
|
|
1875
|
+
sigs.push(` ${mem.text}`);
|
|
1876
|
+
anchors.push([lineAt(stripped, bodyStart + mem.start), lineAt(stripped, bodyStart + mem.end)]);
|
|
1877
|
+
}
|
|
1825
1878
|
}
|
|
1826
|
-
|
|
1879
|
+
|
|
1827
1880
|
// Exported type aliases
|
|
1828
1881
|
for (const m of stripped.matchAll(/^export\s+type\s+(\w+)(?:<[^=]*>)?\s*=/gm)) {
|
|
1829
1882
|
sigs.push(`export type ${m[1]}`);
|
|
1883
|
+
anchors.push([lineAt(stripped, m.index), lineAt(stripped, m.index + m[0].length)]);
|
|
1830
1884
|
}
|
|
1831
|
-
|
|
1885
|
+
|
|
1832
1886
|
// Exported enums
|
|
1833
1887
|
for (const m of stripped.matchAll(/^export\s+(?:const\s+)?enum\s+(\w+)\s*\{/gm)) {
|
|
1888
|
+
const bodyStart = m.index + m[0].length;
|
|
1834
1889
|
sigs.push(`export enum ${m[1]}`);
|
|
1890
|
+
anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
|
|
1835
1891
|
}
|
|
1836
|
-
|
|
1892
|
+
|
|
1837
1893
|
// Classes (exported and internal)
|
|
1838
1894
|
const classRegex = /^(export\s+)?(abstract\s+)?class\s+(\w+)(?:<[^{]*>)?(?:\s+extends\s+[\w<>, .]+)?(?:\s+implements\s+[\w<> ,]+)?\s*\{/gm;
|
|
1839
1895
|
for (const m of stripped.matchAll(classRegex)) {
|
|
1840
1896
|
const prefix = m[1] ? 'export ' : '';
|
|
1841
1897
|
const abs = m[2] ? 'abstract ' : '';
|
|
1898
|
+
const bodyStart = m.index + m[0].length;
|
|
1842
1899
|
sigs.push(`${prefix}${abs}class ${m[3]}`);
|
|
1843
|
-
|
|
1844
|
-
const block = extractBlock(stripped,
|
|
1900
|
+
anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
|
|
1901
|
+
const block = extractBlock(stripped, bodyStart);
|
|
1845
1902
|
const methods = extractClassMembers(block);
|
|
1846
|
-
for (const meth of methods)
|
|
1903
|
+
for (const meth of methods) {
|
|
1904
|
+
sigs.push(` ${meth.text}`);
|
|
1905
|
+
anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
|
|
1906
|
+
}
|
|
1847
1907
|
}
|
|
1848
|
-
|
|
1908
|
+
|
|
1849
1909
|
// Exported top-level functions (not methods)
|
|
1850
1910
|
for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{]+)?\s*\{/gm)) {
|
|
1851
1911
|
const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
|
|
1852
1912
|
const params = normalizeParams(m[2]);
|
|
1853
1913
|
const retMatch = m[0].match(/\)\s*:\s*([^{]+)\s*\{/);
|
|
1854
1914
|
const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 30) : '';
|
|
1855
|
-
const retStr = retType ? `
|
|
1915
|
+
const retStr = retType ? ` → ${retType}` : '';
|
|
1916
|
+
const bodyStart = m.index + m[0].length;
|
|
1856
1917
|
sigs.push(`export ${asyncKw}function ${m[1]}(${params})${retStr}`);
|
|
1918
|
+
anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
|
|
1919
|
+
|
|
1920
|
+
// Hooks: capture compact return object shape for use* functions.
|
|
1921
|
+
if (m[1].startsWith('use')) {
|
|
1922
|
+
const body = stripped.slice(bodyStart, bodyStart + 800);
|
|
1923
|
+
const ret = body.match(/return\s*\{([^}]{1,260})\}/);
|
|
1924
|
+
if (ret) {
|
|
1925
|
+
const keys = ret[1]
|
|
1926
|
+
.split(',')
|
|
1927
|
+
.map((s) => s.trim().split(':')[0].split('(')[0].trim())
|
|
1928
|
+
.filter(Boolean)
|
|
1929
|
+
.slice(0, 8);
|
|
1930
|
+
if (keys.length) {
|
|
1931
|
+
sigs[sigs.length - 1] += ` → { ${keys.join(', ')} }`;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1857
1935
|
}
|
|
1858
|
-
|
|
1936
|
+
|
|
1859
1937
|
// Exported arrow functions / const functions
|
|
1860
1938
|
for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=>{]+)?\s*=>/gm)) {
|
|
1861
1939
|
const asyncKw = /=\s*async\s+/.test(m[0]) ? 'async ' : '';
|
|
1862
1940
|
const params = normalizeParams(m[2]);
|
|
1863
1941
|
sigs.push(`export const ${m[1]} = ${asyncKw}(${params}) =>`);
|
|
1942
|
+
const bodyStart = stripped.indexOf('{', m.index + m[0].length);
|
|
1943
|
+
const endLn = bodyStart !== -1
|
|
1944
|
+
? lineAt(stripped, blockEndIdx(bodyStart + 1))
|
|
1945
|
+
: lineAt(stripped, m.index + m[0].length);
|
|
1946
|
+
anchors.push([lineAt(stripped, m.index), endLn]);
|
|
1947
|
+
|
|
1948
|
+
// Hooks: capture compact return object shape for use* functions.
|
|
1949
|
+
if (m[1].startsWith('use')) {
|
|
1950
|
+
if (bodyStart !== -1) {
|
|
1951
|
+
const body = stripped.slice(bodyStart, bodyStart + 800);
|
|
1952
|
+
const ret = body.match(/return\s*\{([^}]{1,260})\}/);
|
|
1953
|
+
if (ret) {
|
|
1954
|
+
const keys = ret[1]
|
|
1955
|
+
.split(',')
|
|
1956
|
+
.map((s) => s.trim().split(':')[0].split('(')[0].trim())
|
|
1957
|
+
.filter(Boolean)
|
|
1958
|
+
.slice(0, 8);
|
|
1959
|
+
if (keys.length) {
|
|
1960
|
+
sigs[sigs.length - 1] += ` → { ${keys.join(', ')} }`;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1864
1965
|
}
|
|
1865
1966
|
|
|
1866
1967
|
// Zustand stores: export const useXxxStore = create<State>()(...)
|
|
1867
1968
|
for (const m of stripped.matchAll(/^export\s+const\s+(use\w+Store)\s*=\s*create(?:<[^>]*>)?\s*\(/gm)) {
|
|
1868
|
-
// Extract the State interface name from create<StateName>
|
|
1869
1969
|
const stateType = m[0].match(/create<([\w]+)>/)?.[1] || '';
|
|
1970
|
+
const startLn = lineAt(stripped, m.index);
|
|
1870
1971
|
sigs.push(`export const ${m[1]} = create<${stateType}>(...)`);
|
|
1871
|
-
|
|
1972
|
+
anchors.push([startLn, startLn]);
|
|
1872
1973
|
const ifaceRe = new RegExp(`interface\\s+${stateType}\\s*\\{([\\s\\S]*?)\\}`);
|
|
1873
1974
|
const ifm = stripped.match(ifaceRe);
|
|
1874
1975
|
if (ifm) {
|
|
1875
|
-
for (const fm of ifm[1].matchAll(/^\s+(\w+)\s*(?:\([^)]*\))?\s*:/gm)) {
|
|
1876
|
-
sigs.push(` ${fm[1]}`);
|
|
1877
|
-
}
|
|
1976
|
+
for (const fm of ifm[1].matchAll(/^\s+(\w+)\s*(?:\([^)]*\))?\s*:/gm)) { sigs.push(` ${fm[1]}`); anchors.push(null); }
|
|
1878
1977
|
}
|
|
1879
1978
|
}
|
|
1880
1979
|
|
|
1881
|
-
// API client objects:
|
|
1882
|
-
// Pattern: const xxxApi = { methodName: async ... }
|
|
1980
|
+
// API client objects: const xxxApi = { method: async () => {} }
|
|
1883
1981
|
for (const m of stripped.matchAll(/^(?:export\s+default\s+|const\s+)(\w*[Aa]pi\w*)\s*=\s*\{/gm)) {
|
|
1884
|
-
const
|
|
1885
|
-
const
|
|
1886
|
-
const
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1982
|
+
const bodyStart = m.index + m[0].length;
|
|
1983
|
+
const block = extractBlock(stripped, bodyStart);
|
|
1984
|
+
const methods = [...block.matchAll(/^\s+(\w+)\s*:\s*(?:async\s+)?(?:\([^)]*\)|\w+)\s*=>/gm)].map(mm => mm[1]);
|
|
1985
|
+
if (methods.length) {
|
|
1986
|
+
sigs.push(`${m[1]}: { ${methods.join(', ')} }`);
|
|
1987
|
+
anchors.push([lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)]);
|
|
1890
1988
|
}
|
|
1891
|
-
if (methods.length) sigs.push(`${apiName}: { ${methods.join(', ')} }`);
|
|
1892
1989
|
}
|
|
1893
|
-
|
|
1894
|
-
return sigs
|
|
1990
|
+
|
|
1991
|
+
return sigs
|
|
1992
|
+
.map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s))
|
|
1993
|
+
.slice(0, 35);
|
|
1895
1994
|
}
|
|
1896
|
-
|
|
1995
|
+
|
|
1897
1996
|
function extractBlock(src, startIndex) {
|
|
1898
1997
|
let depth = 1;
|
|
1899
1998
|
let i = startIndex;
|
|
@@ -1905,47 +2004,59 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
|
|
|
1905
2004
|
}
|
|
1906
2005
|
return src.slice(startIndex, i - 1);
|
|
1907
2006
|
}
|
|
1908
|
-
|
|
2007
|
+
|
|
2008
|
+
// Returns members as { text, start, end } where start/end are char offsets
|
|
2009
|
+
// WITHIN `block`, so the caller can resolve member line anchors.
|
|
1909
2010
|
function extractInterfaceMembers(block) {
|
|
1910
2011
|
const members = [];
|
|
1911
2012
|
for (const m of block.matchAll(/^\s+(readonly\s+)?(\w+)(\??):\s*([^;]+);/gm)) {
|
|
1912
2013
|
const readonly = m[1] ? 'readonly ' : '';
|
|
1913
2014
|
const optional = m[3] ? '?' : '';
|
|
1914
2015
|
const typeStr = m[4].trim().replace(/\s+/g, ' ').slice(0, 35);
|
|
1915
|
-
|
|
2016
|
+
const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
|
|
2017
|
+
members.push({ text: `${readonly}${m[2]}${optional}: ${typeStr}`, start, end: m.index + m[0].length });
|
|
1916
2018
|
}
|
|
1917
2019
|
for (const m of block.matchAll(/^\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)\s*:/gm)) {
|
|
1918
|
-
|
|
2020
|
+
const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
|
|
2021
|
+
members.push({ text: `${m[1]}(${normalizeParams(m[2])})`, start, end: m.index + m[0].length });
|
|
1919
2022
|
}
|
|
1920
2023
|
return members.slice(0, 8);
|
|
1921
2024
|
}
|
|
1922
|
-
|
|
2025
|
+
|
|
2026
|
+
const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'catch', 'finally', 'else', 'return']);
|
|
2027
|
+
|
|
2028
|
+
// Returns members as { text, start, end } where start/end are char offsets
|
|
2029
|
+
// WITHIN `block` (end = the method's closing brace), so the caller can resolve
|
|
2030
|
+
// per-method line anchors that span the method body.
|
|
1923
2031
|
function extractClassMembers(block) {
|
|
1924
2032
|
const members = [];
|
|
1925
|
-
// Public methods (skip private/protected/_ prefixed)
|
|
2033
|
+
// Public methods (skip private/protected/_ prefixed and control-flow keywords)
|
|
1926
2034
|
const methodRe = /^\s+(?:public\s+|static\s+|async\s+|override\s+)*(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{;]+)?\s*\{/gm;
|
|
1927
2035
|
for (const m of block.matchAll(methodRe)) {
|
|
2036
|
+
if (_CTRL_KEYWORDS.has(m[1])) continue;
|
|
1928
2037
|
if (/^(private|protected|_)/.test(m[1])) continue;
|
|
1929
|
-
|
|
2038
|
+
const bodyStart = m.index + m[0].length; // just past the opening brace
|
|
2039
|
+
const end = bodyStart + extractBlock(block, bodyStart).length;
|
|
2040
|
+
const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
|
|
2041
|
+
if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
|
|
1930
2042
|
const isAsync = m[0].includes('async ') ? 'async ' : '';
|
|
1931
2043
|
const isStatic = m[0].includes('static ') ? 'static ' : '';
|
|
1932
2044
|
const retMatch = m[0].match(/\)\s*:\s*([^{;]+)\s*\{/);
|
|
1933
2045
|
const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 20) : '';
|
|
1934
|
-
const retStr = retType ? `
|
|
1935
|
-
members.push(`${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}
|
|
2046
|
+
const retStr = retType ? ` → ${retType}` : '';
|
|
2047
|
+
members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
|
|
1936
2048
|
}
|
|
1937
2049
|
return members.slice(0, 8);
|
|
1938
2050
|
}
|
|
1939
|
-
|
|
2051
|
+
|
|
1940
2052
|
function normalizeParams(params) {
|
|
1941
2053
|
if (!params) return '';
|
|
1942
2054
|
return params.trim().replace(/\s+/g, ' ').replace(/:[^,)]+/g, '').trim();
|
|
1943
2055
|
}
|
|
1944
|
-
|
|
2056
|
+
|
|
1945
2057
|
module.exports = { extract };
|
|
1946
|
-
|
|
1947
|
-
};
|
|
1948
2058
|
|
|
2059
|
+
};
|
|
1949
2060
|
// ── ./src/extractors/graphql ──
|
|
1950
2061
|
__factories["./src/extractors/graphql"] = function(module, exports) {
|
|
1951
2062
|
|
|
@@ -4052,6 +4163,96 @@ __factories["./src/format/benchmark-report"] = function(module, exports) {
|
|
|
4052
4163
|
}).join('');
|
|
4053
4164
|
}
|
|
4054
4165
|
|
|
4166
|
+
function escapeAttr(s) {
|
|
4167
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
|
|
4168
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
|
4169
|
+
));
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
function readTokenReduction(cwd) {
|
|
4173
|
+
const p = path.join(cwd, 'benchmarks', 'reports', 'token-reduction.json');
|
|
4174
|
+
let data;
|
|
4175
|
+
try { data = JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
|
|
4176
|
+
const repos = Array.isArray(data.repos) ? data.repos : [];
|
|
4177
|
+
if (repos.length === 0) return null;
|
|
4178
|
+
let baseline = 0, signatures = 0, surgical = 0, hasSurgical = false;
|
|
4179
|
+
for (const r of repos) {
|
|
4180
|
+
baseline += toNumber(r.rawTokens) || 0;
|
|
4181
|
+
signatures += toNumber(r.finalTokens) || 0;
|
|
4182
|
+
const s = toNumber(r.surgicalTokens);
|
|
4183
|
+
if (s !== null) { surgical += s; hasSurgical = true; }
|
|
4184
|
+
}
|
|
4185
|
+
const out = {
|
|
4186
|
+
version: data.version || null,
|
|
4187
|
+
repoCount: repos.length,
|
|
4188
|
+
baseline,
|
|
4189
|
+
signatures,
|
|
4190
|
+
savedPct: baseline > 0 ? Math.round((1 - signatures / baseline) * 1000) / 10 : 0,
|
|
4191
|
+
perRepo: repos.map((r) => ({
|
|
4192
|
+
repo: r.repo, language: r.language,
|
|
4193
|
+
rawTokens: toNumber(r.rawTokens) || 0,
|
|
4194
|
+
finalTokens: toNumber(r.finalTokens) || 0,
|
|
4195
|
+
reductionPct: toNumber(r.reductionPct) || 0,
|
|
4196
|
+
})),
|
|
4197
|
+
};
|
|
4198
|
+
if (hasSurgical) {
|
|
4199
|
+
out.surgical = surgical;
|
|
4200
|
+
out.surgicalSavedPct = baseline > 0 ? Math.round((1 - surgical / baseline) * 1000) / 10 : 0;
|
|
4201
|
+
}
|
|
4202
|
+
return out;
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
function tokenReductionPanelHtml(tr) {
|
|
4206
|
+
if (!tr) {
|
|
4207
|
+
return '<div class="panel"><div class="label">Token Reduction</div>' +
|
|
4208
|
+
'<div class="value" style="font-size:13px">No token-reduction benchmark found — run the token benchmark to populate this panel.</div></div>';
|
|
4209
|
+
}
|
|
4210
|
+
const fmt = (n) => Number(n).toLocaleString('en-US');
|
|
4211
|
+
const tiers = [
|
|
4212
|
+
{ label: 'Whole-file baseline', value: fmt(tr.baseline) + ' tok' },
|
|
4213
|
+
{ label: 'Ranked signatures (ask)', value: fmt(tr.signatures) + ' tok' },
|
|
4214
|
+
];
|
|
4215
|
+
if (tr.surgical != null) {
|
|
4216
|
+
tiers.push({ label: 'Surgical (index + delta)', value: fmt(tr.surgical) + ' tok' });
|
|
4217
|
+
tiers.push({ label: 'Saved (surgical)', value: tr.surgicalSavedPct + '%' });
|
|
4218
|
+
} else {
|
|
4219
|
+
tiers.push({ label: 'Saved', value: tr.savedPct + '%' });
|
|
4220
|
+
}
|
|
4221
|
+
const tierHtml = tiers.map((t) =>
|
|
4222
|
+
`<div class="card"><div class="label">${escapeAttr(t.label)}</div><div class="value">${escapeAttr(t.value)}</div></div>`
|
|
4223
|
+
).join('');
|
|
4224
|
+
const sigPct = tr.baseline > 0 ? Math.max(0.4, (tr.signatures / tr.baseline) * 100) : 0;
|
|
4225
|
+
const surgPct = (tr.surgical != null && tr.baseline > 0) ? Math.max(0.4, (tr.surgical / tr.baseline) * 100) : null;
|
|
4226
|
+
const barRow = (label, pct, color) =>
|
|
4227
|
+
`<div style="margin:4px 0;font-size:11px;color:#8ea0d9">${escapeAttr(label)}</div>` +
|
|
4228
|
+
`<div style="background:#0a0f1e;border:1px solid #223056;border-radius:6px;height:14px;overflow:hidden">` +
|
|
4229
|
+
`<div style="width:${pct.toFixed(1)}%;height:100%;background:${color}"></div></div>`;
|
|
4230
|
+
const bars = [
|
|
4231
|
+
barRow('Whole-file baseline (100%)', 100, '#3a4a78'),
|
|
4232
|
+
barRow(`Ranked signatures — ${tr.savedPct}% saved`, sigPct, '#2e7d6b'),
|
|
4233
|
+
surgPct != null ? barRow(`Surgical — ${tr.surgicalSavedPct}% saved`, surgPct, '#5ad1a8') : '',
|
|
4234
|
+
].filter(Boolean).join('');
|
|
4235
|
+
const rows = tr.perRepo.slice(0, 8).map((r) =>
|
|
4236
|
+
`<tr><td>${escapeAttr(r.repo)}</td><td>${escapeAttr(r.language)}</td>` +
|
|
4237
|
+
`<td style="text-align:right">${fmt(r.rawTokens)}</td>` +
|
|
4238
|
+
`<td style="text-align:right">${fmt(r.finalTokens)}</td>` +
|
|
4239
|
+
`<td style="text-align:right">${r.reductionPct}%</td></tr>`
|
|
4240
|
+
).join('');
|
|
4241
|
+
return [
|
|
4242
|
+
'<div class="panel">',
|
|
4243
|
+
`<div class="label">Token Reduction — ${tr.repoCount} benchmark repos${tr.version ? ' · v' + escapeAttr(tr.version) : ''}</div>`,
|
|
4244
|
+
`<div class="grid" style="margin:8px 0">${tierHtml}</div>`,
|
|
4245
|
+
bars,
|
|
4246
|
+
'<table style="width:100%;border-collapse:collapse;margin-top:10px;font-size:12px">',
|
|
4247
|
+
'<thead><tr style="color:#8ea0d9;text-align:left">',
|
|
4248
|
+
'<th>Repo</th><th>Lang</th><th style="text-align:right">Baseline</th><th style="text-align:right">Signatures</th><th style="text-align:right">Saved</th>',
|
|
4249
|
+
'</tr></thead>',
|
|
4250
|
+
`<tbody>${rows}</tbody>`,
|
|
4251
|
+
'</table>',
|
|
4252
|
+
'</div>',
|
|
4253
|
+
].join('');
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4055
4256
|
function buildDashboardData(cwd, health) {
|
|
4056
4257
|
const entries = readLog(cwd);
|
|
4057
4258
|
const recent = entries.slice(-30);
|
|
@@ -4070,15 +4271,18 @@ __factories["./src/format/benchmark-report"] = function(module, exports) {
|
|
|
4070
4271
|
overBudgetStreak: overBudgetStreak(entries),
|
|
4071
4272
|
extractorCoverage: coverage.pct,
|
|
4072
4273
|
};
|
|
4274
|
+
const tokenReduction = readTokenReduction(cwd);
|
|
4073
4275
|
return {
|
|
4074
4276
|
summary,
|
|
4075
4277
|
tokenReductionTrend,
|
|
4076
4278
|
hitAt5Trend,
|
|
4077
4279
|
coverage,
|
|
4280
|
+
tokenReduction,
|
|
4078
4281
|
charts: {
|
|
4079
4282
|
tokenReductionSvg: lineChartSvg(tokenReductionTrend, 'Token reduction trend (last 30 tracked runs)', '%'),
|
|
4080
4283
|
hitAt5Svg: lineChartSvg(hitAt5Trend, 'hit@5 trend (last 30 benchmark runs)', ''),
|
|
4081
4284
|
coverageSvg: barChartSvg(coverage.perLanguage),
|
|
4285
|
+
tokenSavingsPanel: tokenReductionPanelHtml(tokenReduction),
|
|
4082
4286
|
},
|
|
4083
4287
|
};
|
|
4084
4288
|
}
|
|
@@ -4112,6 +4316,7 @@ __factories["./src/format/benchmark-report"] = function(module, exports) {
|
|
|
4112
4316
|
'<h1>SigMap v2.10 dashboard</h1>',
|
|
4113
4317
|
'<div class="sub">Self-contained report. No external scripts, styles, or network calls.</div>',
|
|
4114
4318
|
`<div class="grid">${cardHtml}</div>`,
|
|
4319
|
+
data.charts.tokenSavingsPanel,
|
|
4115
4320
|
`<div class="panel">${data.charts.tokenReductionSvg}</div>`,
|
|
4116
4321
|
`<div class="panel">${data.charts.hitAt5Svg}</div>`,
|
|
4117
4322
|
`<div class="panel">${data.charts.coverageSvg}</div>`,
|
|
@@ -5719,7 +5924,56 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
5719
5924
|
}
|
|
5720
5925
|
}
|
|
5721
5926
|
|
|
5722
|
-
|
|
5927
|
+
function getLines(args, cwd) {
|
|
5928
|
+
if (!args || !args.file) return 'Missing required argument: file';
|
|
5929
|
+
|
|
5930
|
+
const rel = String(args.file).replace(/\\/g, '/').replace(/^\//, '');
|
|
5931
|
+
const abs = path.resolve(cwd, rel);
|
|
5932
|
+
|
|
5933
|
+
// Sandbox: refuse paths that resolve outside the project root.
|
|
5934
|
+
const root = path.resolve(cwd);
|
|
5935
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
5936
|
+
return `Refused: ${rel} resolves outside the project root`;
|
|
5937
|
+
}
|
|
5938
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
5939
|
+
return `File not found: ${rel}`;
|
|
5940
|
+
}
|
|
5941
|
+
|
|
5942
|
+
const start = parseInt(args.start, 10);
|
|
5943
|
+
const end = parseInt(args.end, 10);
|
|
5944
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
5945
|
+
return 'Arguments "start" and "end" must be numbers (1-based line numbers).';
|
|
5946
|
+
}
|
|
5947
|
+
|
|
5948
|
+
let lines;
|
|
5949
|
+
try {
|
|
5950
|
+
lines = fs.readFileSync(abs, 'utf8').split('\n');
|
|
5951
|
+
} catch (err) {
|
|
5952
|
+
return `Could not read ${rel}: ${err.message}`;
|
|
5953
|
+
}
|
|
5954
|
+
|
|
5955
|
+
const total = lines.length;
|
|
5956
|
+
const from = Math.max(1, Math.min(start, end));
|
|
5957
|
+
const to = Math.min(total, Math.max(start, end));
|
|
5958
|
+
if (from > total) return `${rel} has only ${total} lines; requested ${start}-${end}`;
|
|
5959
|
+
|
|
5960
|
+
const slice = lines.slice(from - 1, to);
|
|
5961
|
+
|
|
5962
|
+
let safeLines = slice;
|
|
5963
|
+
try {
|
|
5964
|
+
const { scan } = __require('./src/security/scanner');
|
|
5965
|
+
safeLines = scan(slice, rel).safe;
|
|
5966
|
+
} catch (_) {}
|
|
5967
|
+
|
|
5968
|
+
return [
|
|
5969
|
+
`# ${rel}:${from}-${to}`,
|
|
5970
|
+
'```',
|
|
5971
|
+
...safeLines,
|
|
5972
|
+
'```',
|
|
5973
|
+
].join('\n');
|
|
5974
|
+
}
|
|
5975
|
+
|
|
5976
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines };
|
|
5723
5977
|
};
|
|
5724
5978
|
|
|
5725
5979
|
// ── ./src/learning/weights ──
|
|
@@ -5890,17 +6144,17 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
5890
6144
|
*
|
|
5891
6145
|
* Supported methods:
|
|
5892
6146
|
* initialize → serverInfo + capabilities
|
|
5893
|
-
* tools/list →
|
|
6147
|
+
* tools/list → 10 tool definitions
|
|
5894
6148
|
* tools/call → dispatch to handler, return result
|
|
5895
6149
|
*/
|
|
5896
|
-
|
|
6150
|
+
|
|
5897
6151
|
const readline = require('readline');
|
|
5898
6152
|
const { TOOLS } = __require('./src/mcp/tools');
|
|
5899
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact } = __require('./src/mcp/handlers');
|
|
5900
|
-
|
|
6153
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines } = __require('./src/mcp/handlers');
|
|
6154
|
+
|
|
5901
6155
|
const SERVER_INFO = {
|
|
5902
6156
|
name: 'sigmap',
|
|
5903
|
-
version: '6.
|
|
6157
|
+
version: '6.13.0',
|
|
5904
6158
|
description: 'SigMap MCP server — code signatures on demand',
|
|
5905
6159
|
};
|
|
5906
6160
|
|
|
@@ -5957,6 +6211,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
5957
6211
|
else if (name === 'list_modules') text = listModules(args, cwd);
|
|
5958
6212
|
else if (name === 'query_context') text = queryContext(args, cwd);
|
|
5959
6213
|
else if (name === 'get_impact') text = getImpact(args, cwd);
|
|
6214
|
+
else if (name === 'get_lines') text = getLines(args, cwd);
|
|
5960
6215
|
else {
|
|
5961
6216
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
5962
6217
|
return;
|
|
@@ -6184,8 +6439,36 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
6184
6439
|
required: ['file'],
|
|
6185
6440
|
},
|
|
6186
6441
|
},
|
|
6442
|
+
{
|
|
6443
|
+
name: 'get_lines',
|
|
6444
|
+
description:
|
|
6445
|
+
'Fetch an exact line range from a source file on demand — the Surgical Context ' +
|
|
6446
|
+
'workhorse. Signatures carry `path:start-end` anchors; call this to read just those ' +
|
|
6447
|
+
'lines instead of re-opening the whole file. Lines are clamped to the file bounds and ' +
|
|
6448
|
+
'secret-scanned (redacted) before return. Path is sandboxed to the project root.',
|
|
6449
|
+
inputSchema: {
|
|
6450
|
+
type: 'object',
|
|
6451
|
+
properties: {
|
|
6452
|
+
file: {
|
|
6453
|
+
type: 'string',
|
|
6454
|
+
description:
|
|
6455
|
+
'Relative path from the project root (e.g. "src/config/loader.js"). ' +
|
|
6456
|
+
'Use the path shown in a signature anchor. Use forward slashes.',
|
|
6457
|
+
},
|
|
6458
|
+
start: {
|
|
6459
|
+
type: 'number',
|
|
6460
|
+
description: '1-based start line (inclusive). Clamped to the file bounds.',
|
|
6461
|
+
},
|
|
6462
|
+
end: {
|
|
6463
|
+
type: 'number',
|
|
6464
|
+
description: '1-based end line (inclusive). Clamped to the file bounds.',
|
|
6465
|
+
},
|
|
6466
|
+
},
|
|
6467
|
+
required: ['file', 'start', 'end'],
|
|
6468
|
+
},
|
|
6469
|
+
},
|
|
6187
6470
|
];
|
|
6188
|
-
|
|
6471
|
+
|
|
6189
6472
|
module.exports = { TOOLS };
|
|
6190
6473
|
|
|
6191
6474
|
};
|
|
@@ -8695,7 +8978,7 @@ const path = require('path');
|
|
|
8695
8978
|
const os = require('os');
|
|
8696
8979
|
const { execSync } = require('child_process');
|
|
8697
8980
|
|
|
8698
|
-
const VERSION = '6.
|
|
8981
|
+
const VERSION = '6.13.0';
|
|
8699
8982
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
8700
8983
|
|
|
8701
8984
|
function requireSourceOrBundled(key) {
|
|
@@ -8982,13 +9265,42 @@ function computeEffectiveMaxTokens(fileEntries, config) {
|
|
|
8982
9265
|
|
|
8983
9266
|
function applyTokenBudget(fileEntries, maxTokens) {
|
|
8984
9267
|
// fileEntries: [{ filePath, sigs, mtime }]
|
|
8985
|
-
//
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
9268
|
+
// Per-file rendered overhead: "### <path>", code fences, trailing blank line.
|
|
9269
|
+
// A signature-only budget undercounts this, so when many files survive the
|
|
9270
|
+
// section headers alone can blow maxTokens.
|
|
9271
|
+
const sectionOverhead = (e) => estimateTokens(String(e.filePath || '')) + 6;
|
|
9272
|
+
const renderedTotal = (entries) =>
|
|
9273
|
+
entries.reduce((s, e) => s + estimateTokens(e.sigs.join('\n')) + sectionOverhead(e), 0);
|
|
9274
|
+
// The generated file also carries a ~150-token fixed preamble (SigMap command
|
|
9275
|
+
// table + headers). Reserve at least that much so small budgets don't overflow;
|
|
9276
|
+
// for large budgets this matches the historical 10% reserve.
|
|
9277
|
+
const budgetForEntries = Math.max(1, maxTokens - Math.max(200, Math.ceil(maxTokens * 0.10)));
|
|
9278
|
+
let total = renderedTotal(fileEntries);
|
|
9279
|
+
if (total <= budgetForEntries) return fileEntries;
|
|
9280
|
+
|
|
9281
|
+
// v6.12 Surgical Context — progressive disclosure: before dropping whole files,
|
|
9282
|
+
// collapse signature BODIES to their line-anchor pointers (keep `symbol :start-end`).
|
|
9283
|
+
// Only sigs that actually carry an anchor shrink; the agent can re-fetch bodies via
|
|
9284
|
+
// the get_lines MCP tool. This degrades gracefully instead of losing files outright.
|
|
9285
|
+
let working = fileEntries;
|
|
9286
|
+
const collapsed = fileEntries.map((e) => {
|
|
9287
|
+
const slim = (e.sigs || []).map((s) => {
|
|
9288
|
+
const line = toIndexLine(s);
|
|
9289
|
+
return line && /:\d+-\d+/.test(line) ? line : s; // replace only when it yields an anchor
|
|
9290
|
+
});
|
|
9291
|
+
return { ...e, sigs: slim };
|
|
9292
|
+
});
|
|
9293
|
+
const collapsedRendered = renderedTotal(collapsed);
|
|
9294
|
+
if (collapsedRendered < total) {
|
|
9295
|
+
console.warn(`[sigmap] budget: collapsed bodies to anchors, reclaimed ~${total - collapsedRendered} tokens`);
|
|
9296
|
+
working = collapsed;
|
|
9297
|
+
total = collapsedRendered;
|
|
9298
|
+
// Collapsing keeps every file, so the section overhead must fit too — not just sigs.
|
|
9299
|
+
if (total <= budgetForEntries) return working;
|
|
9300
|
+
}
|
|
8989
9301
|
|
|
8990
9302
|
// Sort by drop priority (drop first = index 0)
|
|
8991
|
-
const withPriority =
|
|
9303
|
+
const withPriority = working.map((e) => {
|
|
8992
9304
|
let priority = 0;
|
|
8993
9305
|
let dropReason = 'budget: low recency';
|
|
8994
9306
|
if (isGeneratedFile(e.filePath)) { priority = 10; dropReason = 'budget: generated file'; }
|
|
@@ -9011,12 +9323,13 @@ function applyTokenBudget(fileEntries, maxTokens) {
|
|
|
9011
9323
|
|
|
9012
9324
|
const kept = [];
|
|
9013
9325
|
const verboseDropped = [];
|
|
9014
|
-
// Iterate forward: highest drop-priority files (generated=10, mock=9, test=8) are at index 0
|
|
9015
|
-
// Drop those first until
|
|
9326
|
+
// Iterate forward: highest drop-priority files (generated=10, mock=9, test=8) are at index 0.
|
|
9327
|
+
// Drop those first until the RENDERED total (sigs + section overhead) fits maxTokens,
|
|
9328
|
+
// then keep everything else.
|
|
9329
|
+
let rendered = renderedTotal(withPriority);
|
|
9016
9330
|
for (const entry of withPriority) {
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
total -= entryTokens;
|
|
9331
|
+
if (rendered > budgetForEntries) {
|
|
9332
|
+
rendered -= estimateTokens(entry.sigs.join('\n')) + sectionOverhead(entry);
|
|
9020
9333
|
verboseDropped.push({ filePath: entry.filePath, reason: entry.dropReason });
|
|
9021
9334
|
} else {
|
|
9022
9335
|
kept.push(entry);
|
|
@@ -10576,6 +10889,40 @@ function buildMiniContext(ranked, cwd) {
|
|
|
10576
10889
|
return lines.join('\n');
|
|
10577
10890
|
}
|
|
10578
10891
|
|
|
10892
|
+
// Surgical Context Phase 2 (v6.12.0): collapse one signature to a symbol-header
|
|
10893
|
+
// pointer — `<declaration head> :start-end` — dropping param lists, return types,
|
|
10894
|
+
// and any body so the agent fetches the real lines on demand via the get_lines MCP tool.
|
|
10895
|
+
function toIndexLine(sig) {
|
|
10896
|
+
if (typeof sig !== 'string') return '';
|
|
10897
|
+
// Anchor is `:start-end`, optionally followed by a `# doc hint` (Python extractor).
|
|
10898
|
+
const m = sig.match(/\s*:(\d+)-(\d+)(?:\s*#.*)?\s*$/);
|
|
10899
|
+
if (!m) {
|
|
10900
|
+
// No anchor (e.g. an indented member) — keep a trimmed head so it stays listed.
|
|
10901
|
+
return sig.trim().slice(0, 60);
|
|
10902
|
+
}
|
|
10903
|
+
const range = `:${m[1]}-${m[2]}`;
|
|
10904
|
+
let head = sig.slice(0, m.index).trim();
|
|
10905
|
+
const paren = head.indexOf('(');
|
|
10906
|
+
if (paren > 0) head = head.slice(0, paren).trim(); // drop params
|
|
10907
|
+
head = head.replace(/\s*[=→].*$/, '').trim(); // drop return type / assignment tail
|
|
10908
|
+
return head ? `${head} ${range}` : range;
|
|
10909
|
+
}
|
|
10910
|
+
|
|
10911
|
+
// Two-tier index output: emit only `file → [symbol:start-end]` headers (no bodies).
|
|
10912
|
+
function buildIndexContext(ranked, cwd) {
|
|
10913
|
+
const lines = [
|
|
10914
|
+
'# SigMap Query Context (index mode)',
|
|
10915
|
+
`Generated: ${new Date().toISOString()}`,
|
|
10916
|
+
'> Symbol index only — fetch exact lines on demand via the `get_lines` MCP tool.',
|
|
10917
|
+
'',
|
|
10918
|
+
];
|
|
10919
|
+
for (const { file, sigs } of ranked) {
|
|
10920
|
+
const idx = sigs.slice(0, 40).map(toIndexLine).filter(Boolean);
|
|
10921
|
+
lines.push(`## ${file}`, '```', ...idx, '```', '');
|
|
10922
|
+
}
|
|
10923
|
+
return lines.join('\n');
|
|
10924
|
+
}
|
|
10925
|
+
|
|
10579
10926
|
function computeCurrentRisk(cwd) {
|
|
10580
10927
|
try {
|
|
10581
10928
|
const { execSync } = require('child_process');
|
|
@@ -10746,7 +11093,7 @@ function main() {
|
|
|
10746
11093
|
const mode = modeIdx !== -1
|
|
10747
11094
|
? args[modeIdx + 1]
|
|
10748
11095
|
: (config.mode || 'default');
|
|
10749
|
-
const VALID_MODES = ['default', 'fast', 'full', 'both'];
|
|
11096
|
+
const VALID_MODES = ['default', 'fast', 'full', 'both', 'index'];
|
|
10750
11097
|
if (mode && !VALID_MODES.includes(mode)) {
|
|
10751
11098
|
console.error(`[sigmap] unknown --mode "${mode}". Valid: ${VALID_MODES.join(', ')}`);
|
|
10752
11099
|
process.exit(1);
|
|
@@ -10825,9 +11172,23 @@ function main() {
|
|
|
10825
11172
|
}
|
|
10826
11173
|
}
|
|
10827
11174
|
|
|
11175
|
+
// v6.12: Delta context — restrict to files changed since a git ref (--since <ref>).
|
|
11176
|
+
const sinceIdx = args.indexOf('--since');
|
|
11177
|
+
if (sinceIdx !== -1 && args[sinceIdx + 1]) {
|
|
11178
|
+
const baseRef = args[sinceIdx + 1];
|
|
11179
|
+
const changed = getFilesChangedSinceBase(cwd, baseRef);
|
|
11180
|
+
const before = ranked.length;
|
|
11181
|
+
ranked = ranked.filter((r) => changed.has(path.resolve(cwd, r.file)));
|
|
11182
|
+
if (!args.includes('--json')) {
|
|
11183
|
+
process.stderr.write(`[sigmap] Δ since ${baseRef}: ${ranked.length}/${before} ranked files changed\n`);
|
|
11184
|
+
}
|
|
11185
|
+
}
|
|
11186
|
+
|
|
10828
11187
|
// v6.8: Save session for future --followup calls
|
|
10829
11188
|
saveSession(cwd, { intent, topFiles: ranked.slice(0, 5).map(r => ({ file: r.file, score: r.score })), query });
|
|
10830
|
-
|
|
11189
|
+
// v6.12: Two-tier output — `--mode index` emits symbol pointers only (bodies via get_lines).
|
|
11190
|
+
const indexMode = mode === 'index' || args.includes('--index');
|
|
11191
|
+
const miniCtx = indexMode ? buildIndexContext(ranked, cwd) : buildMiniContext(ranked, cwd);
|
|
10831
11192
|
const outPath = path.join(cwd, '.context', 'query-context.md');
|
|
10832
11193
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
10833
11194
|
fs.writeFileSync(outPath, miniCtx, 'utf8');
|