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/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
- const block = extractBlock(stripped, m.index + m[0].length);
940
- for (const meth of extractClassMembers(block, returnHints)) sigs.push(` ${meth}`);
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) sigs.push(`module.exports = { ${names.join(', ')} }`);
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.slice(0, 25);
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
- if (m[1] === 'constructor') { members.push(`constructor(${normalizeParams(m[2])})`); continue; }
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 ? ` \u2192 ${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
- // Strip single-line comments
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 start = m.index + m[0].length;
1822
- const block = extractBlock(stripped, start);
1872
+ const block = extractBlock(stripped, bodyStart);
1823
1873
  const members = extractInterfaceMembers(block);
1824
- for (const mem of members) sigs.push(` ${mem}`);
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
- const start = m.index + m[0].length;
1844
- const block = extractBlock(stripped, start);
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) sigs.push(` ${meth}`);
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 ? ` \u2192 ${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
- // Extract action/state keys from the embedded interface in same file
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: export default { method: async (...) => ... }
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 apiName = m[1];
1885
- const start = m.index + m[0].length;
1886
- const block = extractBlock(stripped, start);
1887
- const methods = [];
1888
- for (const mm of block.matchAll(/^\s+(\w+)\s*:\s*(?:async\s+)?(?:\([^)]*\)|\w+)\s*=>/gm)) {
1889
- methods.push(mm[1]);
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.slice(0, 35);
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
- members.push(`${readonly}${m[2]}${optional}: ${typeStr}`);
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
- members.push(`${m[1]}(${normalizeParams(m[2])})`);
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
- if (m[1] === 'constructor') { members.push(`constructor(${normalizeParams(m[2])})`); continue; }
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 ? ` \u2192 ${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
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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
- module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact };
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 → 3 tool definitions
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.11.1',
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.11.1';
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
- // Reserve ~10% for formatting overhead (section headers, code fences, top-level header)
8986
- const effectiveBudget = Math.floor(maxTokens * 0.90);
8987
- let total = fileEntries.reduce((s, e) => s + estimateTokens(e.sigs.join('\n')), 0);
8988
- if (total <= effectiveBudget) return fileEntries;
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 = fileEntries.map((e) => {
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 we're under budget, then keep everything else
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
- const entryTokens = estimateTokens(entry.sigs.join('\n'));
9018
- if (total > effectiveBudget) {
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
- const miniCtx = buildMiniContext(ranked, cwd);
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');