sigmap 6.12.0 → 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 CHANGED
@@ -10,6 +10,25 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [6.13.0] — 2026-06-05
14
+
15
+ ### Added
16
+
17
+ - **Line anchors for JavaScript + member-level anchors (Surgical Context Phase 2.1, #223, PR #224):**
18
+ - The **JavaScript extractor** now emits `:start-end` line anchors on top-level functions, classes, exported arrow functions, and `module.exports` — previously only TypeScript and Python carried anchors. JS block-comment stripping switched to the newline-preserving blank so anchor line numbers stay exact below a `/* … */`.
19
+ - **Class methods and interface members** (TypeScript **and** JavaScript) now carry their **own** `:start-end` anchor spanning the member body, instead of inheriting nothing — unlocking method-level targeting with the `get_lines` MCP tool.
20
+ - Measured effect: index-mode token reduction on real repos rises from ~4.6% to **32–42%** (axios 42.1%, fastify 41.1%, svelte 36.8%, vue-core 32.4%), now 100% anchored.
21
+
22
+ ### Changed
23
+
24
+ - The bundled standalone `gen-context.js` extractor factories are re-synced with `src/` (the bundle had been stale since v6.11.0), so anchors work in the single-file distribution too.
25
+
26
+ ### Fixed
27
+
28
+ - **Token budget could exceed `maxTokens` with many files.** The budget was signature-only and undercounted per-file section headers plus the fixed ~150-token preamble; with anchored (collapsible) signatures keeping more files alive, output could overflow. `applyTokenBudget` now budgets *rendered* cost (signatures + section overhead) against `maxTokens` minus a `max(200, 10%)` preamble reserve.
29
+
30
+ ---
31
+
13
32
  ## [6.12.0] — 2026-06-05
14
33
 
15
34
  ### Added
package/README.md CHANGED
@@ -87,7 +87,7 @@ Ask → Rank → Context → Validate → Judge → Learn
87
87
  ## Benchmark
88
88
 
89
89
  ```
90
- Benchmark : sigmap-v6.12-main (21 repositories, including R language)
90
+ Benchmark : sigmap-v6.13-main (21 repositories, including R language)
91
91
  Date : 2026-06-05
92
92
 
93
93
  Hit@5 : 81.1% (baseline 13.6% — 6.0× lift)
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
 
@@ -6043,7 +6154,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
6043
6154
 
6044
6155
  const SERVER_INFO = {
6045
6156
  name: 'sigmap',
6046
- version: '6.12.0',
6157
+ version: '6.13.0',
6047
6158
  description: 'SigMap MCP server — code signatures on demand',
6048
6159
  };
6049
6160
 
@@ -8867,7 +8978,7 @@ const path = require('path');
8867
8978
  const os = require('os');
8868
8979
  const { execSync } = require('child_process');
8869
8980
 
8870
- const VERSION = '6.12.0';
8981
+ const VERSION = '6.13.0';
8871
8982
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8872
8983
 
8873
8984
  function requireSourceOrBundled(key) {
@@ -9154,10 +9265,18 @@ function computeEffectiveMaxTokens(fileEntries, config) {
9154
9265
 
9155
9266
  function applyTokenBudget(fileEntries, maxTokens) {
9156
9267
  // fileEntries: [{ filePath, sigs, mtime }]
9157
- // Reserve ~10% for formatting overhead (section headers, code fences, top-level header)
9158
- const effectiveBudget = Math.floor(maxTokens * 0.90);
9159
- let total = fileEntries.reduce((s, e) => s + estimateTokens(e.sigs.join('\n')), 0);
9160
- 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;
9161
9280
 
9162
9281
  // v6.12 Surgical Context — progressive disclosure: before dropping whole files,
9163
9282
  // collapse signature BODIES to their line-anchor pointers (keep `symbol :start-end`).
@@ -9171,12 +9290,13 @@ function applyTokenBudget(fileEntries, maxTokens) {
9171
9290
  });
9172
9291
  return { ...e, sigs: slim };
9173
9292
  });
9174
- const collapsedTotal = collapsed.reduce((s, e) => s + estimateTokens(e.sigs.join('\n')), 0);
9175
- if (collapsedTotal < total) {
9176
- console.warn(`[sigmap] budget: collapsed bodies to anchors, reclaimed ~${total - collapsedTotal} tokens`);
9293
+ const collapsedRendered = renderedTotal(collapsed);
9294
+ if (collapsedRendered < total) {
9295
+ console.warn(`[sigmap] budget: collapsed bodies to anchors, reclaimed ~${total - collapsedRendered} tokens`);
9177
9296
  working = collapsed;
9178
- total = collapsedTotal;
9179
- if (total <= effectiveBudget) return working; // anchor collapse alone fit the budget
9297
+ total = collapsedRendered;
9298
+ // Collapsing keeps every file, so the section overhead must fit too — not just sigs.
9299
+ if (total <= budgetForEntries) return working;
9180
9300
  }
9181
9301
 
9182
9302
  // Sort by drop priority (drop first = index 0)
@@ -9203,12 +9323,13 @@ function applyTokenBudget(fileEntries, maxTokens) {
9203
9323
 
9204
9324
  const kept = [];
9205
9325
  const verboseDropped = [];
9206
- // Iterate forward: highest drop-priority files (generated=10, mock=9, test=8) are at index 0
9207
- // 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);
9208
9330
  for (const entry of withPriority) {
9209
- const entryTokens = estimateTokens(entry.sigs.join('\n'));
9210
- if (total > effectiveBudget) {
9211
- total -= entryTokens;
9331
+ if (rendered > budgetForEntries) {
9332
+ rendered -= estimateTokens(entry.sigs.join('\n')) + sectionOverhead(entry);
9212
9333
  verboseDropped.push({ filePath: entry.filePath, reason: entry.dropReason });
9213
9334
  } else {
9214
9335
  kept.push(entry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "6.12.0",
3
+ "version": "6.13.0",
4
4
  "description": "Zero-dependency AI context engine — 97% token reduction. No npm install. Runs on Node 18+.",
5
5
  "main": "gen-context.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "6.12.0",
3
+ "version": "6.13.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "6.12.0",
3
+ "version": "6.13.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,57 +1,88 @@
1
1
  'use strict';
2
2
 
3
+ const { lineAt, withAnchor } = require('./line-anchor');
4
+
3
5
  /**
4
6
  * Extract signatures from JavaScript source code.
7
+ * Top-level declarations and class members carry a `:start-end` line anchor
8
+ * (see line-anchor.js); kept parallel to `sigs` and applied once at return.
5
9
  * @param {string} src - Raw file content
6
10
  * @returns {string[]} Array of signature strings
7
11
  */
8
12
  function extract(src) {
9
13
  if (!src || typeof src !== 'string') return [];
10
14
  const sigs = [];
15
+ const anchors = [];
11
16
  const returnHints = buildReturnHints(src);
12
17
 
18
+ // Block comments are blanked newline-by-newline (non-newline chars → spaces)
19
+ // so character offsets AND line numbers stay exact for anchors.
13
20
  const stripped = src
14
21
  .replace(/\/\/.*$/gm, '')
15
- .replace(/\/\*[\s\S]*?\*\//g, '');
22
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
23
+
24
+ const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;
25
+ // End line for a function whose match ends at `matchEnd` (before its body brace).
26
+ const fnEndLine = (matchEnd, startLn) => {
27
+ const brace = stripped.indexOf('{', matchEnd);
28
+ return brace !== -1 ? lineAt(stripped, blockEndIdx(brace + 1)) : startLn;
29
+ };
16
30
 
17
31
  // Classes
18
32
  const classRegex = /^(export\s+(?:default\s+)?)?class\s+(\w+)(?:\s+extends\s+[\w.]+)?\s*\{/gm;
19
33
  for (const m of stripped.matchAll(classRegex)) {
20
34
  const prefix = m[1] ? m[1].trim() + ' ' : '';
35
+ const bodyStart = m.index + m[0].length;
21
36
  sigs.push(`${prefix}class ${m[2]}`);
22
- const block = extractBlock(stripped, m.index + m[0].length);
23
- for (const meth of extractClassMembers(block, returnHints)) sigs.push(` ${meth}`);
37
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
38
+ const block = extractBlock(stripped, bodyStart);
39
+ for (const meth of extractClassMembers(block, returnHints)) {
40
+ sigs.push(` ${meth.text}`);
41
+ anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
42
+ }
24
43
  }
25
44
 
26
45
  // Exported named functions
27
46
  for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
28
47
  const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
29
48
  const retStr = formatReturnHint(returnHints.get(m[1]));
49
+ const startLn = lineAt(stripped, m.index);
30
50
  sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
51
+ anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
31
52
  }
32
53
 
33
54
  // Exported arrow functions
34
55
  for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/gm)) {
35
56
  const asyncKw = m[0].includes('async') ? 'async ' : '';
36
57
  const retStr = formatReturnHint(returnHints.get(m[1]));
58
+ const startLn = lineAt(stripped, m.index);
37
59
  sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>${retStr}`);
60
+ anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
38
61
  }
39
62
 
40
63
  // module.exports = { ... }
41
64
  const moduleExports = stripped.match(/^module\.exports\s*=\s*\{([^}]+)\}/m);
42
65
  if (moduleExports) {
43
66
  const names = moduleExports[1].split(',').map((s) => s.trim()).filter(Boolean);
44
- if (names.length > 0) sigs.push(`module.exports = { ${names.join(', ')} }`);
67
+ if (names.length > 0) {
68
+ const startLn = lineAt(stripped, moduleExports.index);
69
+ sigs.push(`module.exports = { ${names.join(', ')} }`);
70
+ anchors.push([startLn, lineAt(stripped, moduleExports.index + moduleExports[0].length)]);
71
+ }
45
72
  }
46
73
 
47
74
  // Top-level named functions (non-exported)
48
75
  for (const m of stripped.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
49
76
  const asyncKw = m[0].startsWith('async') ? 'async ' : '';
50
77
  const retStr = formatReturnHint(returnHints.get(m[1]));
78
+ const startLn = lineAt(stripped, m.index);
51
79
  sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
80
+ anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
52
81
  }
53
82
 
54
- return sigs.slice(0, 25);
83
+ return sigs
84
+ .map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s))
85
+ .slice(0, 25);
55
86
  }
56
87
 
57
88
  function extractBlock(src, startIndex) {
@@ -66,15 +97,21 @@ function extractBlock(src, startIndex) {
66
97
  return src.slice(startIndex, i - 1);
67
98
  }
68
99
 
100
+ // Returns members as { text, start, end } where start/end are char offsets
101
+ // WITHIN `block` (end = the method's closing brace), so the caller can resolve
102
+ // per-method line anchors that span the method body.
69
103
  function extractClassMembers(block, returnHints) {
70
104
  const members = [];
71
105
  for (const m of block.matchAll(/^\s+(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(([^)]*)\)\s*\{/gm)) {
72
106
  if (/^_/.test(m[1])) continue;
73
- if (m[1] === 'constructor') { members.push(`constructor(${normalizeParams(m[2])})`); continue; }
107
+ const bodyStart = m.index + m[0].length; // just past the opening brace
108
+ const end = bodyStart + extractBlock(block, bodyStart).length;
109
+ const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
110
+ if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
74
111
  const isAsync = m[0].includes('async ') ? 'async ' : '';
75
112
  const isStatic = m[0].includes('static ') ? 'static ' : '';
76
113
  const retStr = formatReturnHint(returnHints.get(m[1]));
77
- members.push(`${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`);
114
+ members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
78
115
  }
79
116
  return members.slice(0, 8);
80
117
  }
@@ -35,7 +35,10 @@ function extract(src) {
35
35
  // Collect members
36
36
  const block = extractBlock(stripped, bodyStart);
37
37
  const members = extractInterfaceMembers(block);
38
- for (const mem of members) { sigs.push(` ${mem}`); anchors.push(null); }
38
+ for (const mem of members) {
39
+ sigs.push(` ${mem.text}`);
40
+ anchors.push([lineAt(stripped, bodyStart + mem.start), lineAt(stripped, bodyStart + mem.end)]);
41
+ }
39
42
  }
40
43
 
41
44
  // Exported type aliases
@@ -61,7 +64,10 @@ function extract(src) {
61
64
  anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
62
65
  const block = extractBlock(stripped, bodyStart);
63
66
  const methods = extractClassMembers(block);
64
- for (const meth of methods) { sigs.push(` ${meth}`); anchors.push(null); }
67
+ for (const meth of methods) {
68
+ sigs.push(` ${meth.text}`);
69
+ anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
70
+ }
65
71
  }
66
72
 
67
73
  // Exported top-level functions (not methods)
@@ -163,22 +169,29 @@ function extractBlock(src, startIndex) {
163
169
  return src.slice(startIndex, i - 1);
164
170
  }
165
171
 
172
+ // Returns members as { text, start, end } where start/end are char offsets
173
+ // WITHIN `block`, so the caller can resolve member line anchors.
166
174
  function extractInterfaceMembers(block) {
167
175
  const members = [];
168
176
  for (const m of block.matchAll(/^\s+(readonly\s+)?(\w+)(\??):\s*([^;]+);/gm)) {
169
177
  const readonly = m[1] ? 'readonly ' : '';
170
178
  const optional = m[3] ? '?' : '';
171
179
  const typeStr = m[4].trim().replace(/\s+/g, ' ').slice(0, 35);
172
- members.push(`${readonly}${m[2]}${optional}: ${typeStr}`);
180
+ const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
181
+ members.push({ text: `${readonly}${m[2]}${optional}: ${typeStr}`, start, end: m.index + m[0].length });
173
182
  }
174
183
  for (const m of block.matchAll(/^\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)\s*:/gm)) {
175
- members.push(`${m[1]}(${normalizeParams(m[2])})`);
184
+ const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
185
+ members.push({ text: `${m[1]}(${normalizeParams(m[2])})`, start, end: m.index + m[0].length });
176
186
  }
177
187
  return members.slice(0, 8);
178
188
  }
179
189
 
180
190
  const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'catch', 'finally', 'else', 'return']);
181
191
 
192
+ // Returns members as { text, start, end } where start/end are char offsets
193
+ // WITHIN `block` (end = the method's closing brace), so the caller can resolve
194
+ // per-method line anchors that span the method body.
182
195
  function extractClassMembers(block) {
183
196
  const members = [];
184
197
  // Public methods (skip private/protected/_ prefixed and control-flow keywords)
@@ -186,13 +199,16 @@ function extractClassMembers(block) {
186
199
  for (const m of block.matchAll(methodRe)) {
187
200
  if (_CTRL_KEYWORDS.has(m[1])) continue;
188
201
  if (/^(private|protected|_)/.test(m[1])) continue;
189
- if (m[1] === 'constructor') { members.push(`constructor(${normalizeParams(m[2])})`); continue; }
202
+ const bodyStart = m.index + m[0].length; // just past the opening brace
203
+ const end = bodyStart + extractBlock(block, bodyStart).length;
204
+ const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
205
+ if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
190
206
  const isAsync = m[0].includes('async ') ? 'async ' : '';
191
207
  const isStatic = m[0].includes('static ') ? 'static ' : '';
192
208
  const retMatch = m[0].match(/\)\s*:\s*([^{;]+)\s*\{/);
193
209
  const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 20) : '';
194
210
  const retStr = retType ? ` → ${retType}` : '';
195
- members.push(`${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`);
211
+ members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
196
212
  }
197
213
  return members.slice(0, 8);
198
214
  }
package/src/mcp/server.js CHANGED
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '6.12.0',
21
+ version: '6.13.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24