@ctxo/cli 0.14.0 → 0.15.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.
Files changed (41) hide show
  1. package/README.md +10 -1
  2. package/dist/{chunk-ZL36YS2V.js → chunk-A4URKKPP.js} +3 -177
  3. package/dist/{chunk-SRQZDGFZ.js → chunk-BBSJB4CG.js} +2 -2
  4. package/dist/{chunk-X4TVFJ4C.js → chunk-EC6INGPV.js} +1 -1
  5. package/dist/{chunk-7HSA5JVP.js → chunk-I7RPXT6L.js} +193 -1
  6. package/dist/chunk-IT426P3F.js +1036 -0
  7. package/dist/{chunk-G4BMKCKG.js → chunk-JDL6GGTO.js} +16 -5
  8. package/dist/{cli-router-AZCDNX5R.js → cli-router-W56INOQ3.js} +10 -10
  9. package/dist/index.js +3 -3
  10. package/dist/internal.d.ts +90 -3
  11. package/dist/internal.js +4 -4
  12. package/dist/{json-index-reader-DBW32TCN.js → json-index-reader-DKEIP2QR.js} +2 -2
  13. package/dist/{mcp-server-6YG2QE6E.js → mcp-server-IYCCE74E.js} +178 -446
  14. package/dist/module-host-KTGDODES.js +13 -0
  15. package/llms-full.txt +68 -3
  16. package/llms.txt +14 -2
  17. package/package.json +4 -3
  18. package/dist/atomic-write-BAFH464Q.js.map +0 -1
  19. package/dist/chunk-2Q76LH2H.js.map +0 -1
  20. package/dist/chunk-5DSKZJNN.js.map +0 -1
  21. package/dist/chunk-72CEQEQD.js.map +0 -1
  22. package/dist/chunk-7HSA5JVP.js.map +0 -1
  23. package/dist/chunk-G4BMKCKG.js.map +0 -1
  24. package/dist/chunk-L2CAUER6.js +0 -508
  25. package/dist/chunk-L2CAUER6.js.map +0 -1
  26. package/dist/chunk-LAX6VVBU.js.map +0 -1
  27. package/dist/chunk-PSKDDLSF.js.map +0 -1
  28. package/dist/chunk-RFEXMBTG.js.map +0 -1
  29. package/dist/chunk-SRQZDGFZ.js.map +0 -1
  30. package/dist/chunk-X4TVFJ4C.js.map +0 -1
  31. package/dist/chunk-ZL36YS2V.js.map +0 -1
  32. package/dist/cli-router-AZCDNX5R.js.map +0 -1
  33. package/dist/index.js.map +0 -1
  34. package/dist/internal.js.map +0 -1
  35. package/dist/json-index-reader-DBW32TCN.js.map +0 -1
  36. package/dist/mcp-server-6YG2QE6E.js.map +0 -1
  37. package/dist/module-host-KNMKS62R.js +0 -13
  38. package/dist/module-host-KNMKS62R.js.map +0 -1
  39. package/dist/module-loader-B3LYF3W7.js.map +0 -1
  40. package/dist/plugin-discovery-XRXI3IJF.js.map +0 -1
  41. package/dist/staleness-detector-4HJYBQ7Z.js.map +0 -1
package/README.md CHANGED
@@ -36,7 +36,7 @@ It's not a skill gap. It's a **sensory gap**. The agent is navigating your codeb
36
36
 
37
37
  The core shift: your agent stops reacting to files it stumbles into and starts planning from a complete map. **Blast radius before the edit. Git intent before the bug fix. Importer list before the rename.**
38
38
 
39
- Ctxo indexes your repo once, kept fresh by file watchers and git hooks, into a deterministic graph: every symbol, every edge (imports, calls, extends, implements, uses, and the data edges reads, writes and executes), every relevant git commit with intent classified, every anti-pattern. All exposed through 14 semantic MCP tools.
39
+ Ctxo indexes your repo once, kept fresh by file watchers and git hooks, into a deterministic graph: every symbol, every edge (imports, calls, extends, implements, uses, and the data edges reads, writes and executes), every relevant git commit with intent classified, every anti-pattern. All exposed through 14 semantic MCP tools, plus 2 domain tools with `@ctxo/domain`.
40
40
 
41
41
  One `get_blast_radius` call replaces an entire ripgrep/read spiral. One `get_pr_impact` replaces a full review session of "wait, what calls this?"
42
42
 
@@ -154,6 +154,15 @@ ctxo domain rules --write # path-scoped rules that point the age
154
154
  ctxo domain discover # propose tracker prefixes and test conventions from the repo
155
155
  ```
156
156
 
157
+ With cards in place, `ctxo index` links each rule to the tests that pin it and the code that implements it, and the agent sees the rules while it works: `get_blast_radius` lists the rules a change may break and the cards to update, `get_why_context` the decisions behind them, `get_context_for_task` a card excerpt, `get_pr_impact` the contexts touched and the rules without tests. Two tools answer domain questions directly:
158
+
159
+ | Tool | Answers |
160
+ | --- | --- |
161
+ | `get_domain_context` | The card of the context that owns a symbol or file: terms, rules with their tests and code, contracts, decisions |
162
+ | `get_rule` | One rule by ID: the tests that pin it (and how each was found), the symbols that implement it, the decisions behind it |
163
+
164
+ `get_ranked_context` with `scope: "domain"` searches the cards and the documents they link to.
165
+
157
166
  See [Ctxo domain conventions v1](docs/features/domain-knowledge.md#ctxo-domain-conventions-v1) for the registry, the card template and the adoption ladder.
158
167
 
159
168
  ## Supported Languages
@@ -3,7 +3,7 @@ import {
3
3
  DATA_EDGE_KINDS,
4
4
  createTestFileClassifier,
5
5
  filesWithTestAnnotations
6
- } from "./chunk-7HSA5JVP.js";
6
+ } from "./chunk-I7RPXT6L.js";
7
7
  import {
8
8
  compareCodepoint
9
9
  } from "./chunk-72CEQEQD.js";
@@ -12,7 +12,7 @@ import {
12
12
  EDGE_KINDS,
13
13
  EdgeKindSchema,
14
14
  JsonIndexReader
15
- } from "./chunk-X4TVFJ4C.js";
15
+ } from "./chunk-EC6INGPV.js";
16
16
  import {
17
17
  atomicWrite
18
18
  } from "./chunk-RFEXMBTG.js";
@@ -1816,184 +1816,10 @@ function fileOf(symbolId) {
1816
1816
  return symbolId.split("::")[0];
1817
1817
  }
1818
1818
 
1819
- // src/core/search/symbol-tokenizer.ts
1820
- var STOP_WORDS = /* @__PURE__ */ new Set([
1821
- "get",
1822
- "set",
1823
- "is",
1824
- "has",
1825
- "can",
1826
- "do",
1827
- "the",
1828
- "a",
1829
- "an",
1830
- "of",
1831
- "to",
1832
- "in",
1833
- "for",
1834
- "on",
1835
- "with",
1836
- "at",
1837
- "by",
1838
- "from",
1839
- "as",
1840
- "into",
1841
- "not",
1842
- "no",
1843
- "or"
1844
- ]);
1845
- var DEFAULT_OPTIONS = {
1846
- includeStopWords: false,
1847
- includeOriginal: true,
1848
- includeFilePath: false
1849
- };
1850
- var SymbolTokenizer = class _SymbolTokenizer {
1851
- options;
1852
- constructor(options) {
1853
- this.options = { ...DEFAULT_OPTIONS, ...options };
1854
- }
1855
- /**
1856
- * Tokenize a symbol name into searchable tokens.
1857
- *
1858
- * Examples:
1859
- * "getCoChangeMetrics" → ["get", "co", "change", "metrics"] (or without stop words: ["co", "change", "metrics"])
1860
- * "SqliteStorageAdapter" → ["sqlite", "storage", "adapter"]
1861
- * "i_storage_port" → ["i", "storage", "port"]
1862
- * "BM25Scorer" → ["bm", "25", "scorer"]
1863
- * "DB_PASSWORD" → ["db", "password"]
1864
- */
1865
- tokenize(symbolName, filePath) {
1866
- const splitTokens = _SymbolTokenizer.splitName(symbolName);
1867
- const lowered = splitTokens.map((t) => t.toLowerCase());
1868
- const tokens = this.options.includeStopWords ? lowered : lowered.filter((t) => !STOP_WORDS.has(t));
1869
- if (this.options.includeOriginal) {
1870
- const original = symbolName.toLowerCase();
1871
- if (!tokens.includes(original)) {
1872
- tokens.unshift(original);
1873
- }
1874
- }
1875
- if (this.options.includeFilePath && filePath) {
1876
- const pathTokens = _SymbolTokenizer.tokenizeFilePath(filePath);
1877
- for (const pt of pathTokens) {
1878
- if (!tokens.includes(pt)) {
1879
- tokens.push(pt);
1880
- }
1881
- }
1882
- }
1883
- return tokens;
1884
- }
1885
- /**
1886
- * Tokenize a search query. Queries use stop words and don't include originals.
1887
- */
1888
- tokenizeQuery(query) {
1889
- const words = query.trim().split(/\s+/);
1890
- const tokens = [];
1891
- for (const word of words) {
1892
- const split = _SymbolTokenizer.splitName(word);
1893
- for (const t of split) {
1894
- const lowered = t.toLowerCase();
1895
- if (lowered.length > 0 && !tokens.includes(lowered)) {
1896
- tokens.push(lowered);
1897
- }
1898
- }
1899
- }
1900
- return tokens;
1901
- }
1902
- /**
1903
- * Split a name into constituent words based on naming convention boundaries.
1904
- *
1905
- * Rules:
1906
- * 1. Split on underscores and hyphens (snake_case, kebab-case)
1907
- * 2. Split on camelCase/PascalCase boundaries (uppercase after lowercase)
1908
- * 3. Split acronym boundaries (e.g., "HTMLParser" → ["HTML", "Parser"])
1909
- * 4. Split on digit ↔ letter boundaries
1910
- */
1911
- static splitName(name) {
1912
- if (!name || name.length === 0) return [];
1913
- const segments = name.split(/[_\-./\\]+/).filter((s) => s.length > 0);
1914
- const tokens = [];
1915
- for (const segment of segments) {
1916
- tokens.push(..._SymbolTokenizer.splitCamelCase(segment));
1917
- }
1918
- return tokens.filter((t) => t.length > 0);
1919
- }
1920
- /**
1921
- * Split a single segment on camelCase/PascalCase/digit boundaries.
1922
- */
1923
- static splitCamelCase(segment) {
1924
- const tokens = [];
1925
- let current = "";
1926
- for (let i = 0; i < segment.length; i++) {
1927
- const ch = segment[i];
1928
- const prev = i > 0 ? segment[i - 1] : "";
1929
- if (current.length > 0) {
1930
- const prevIsDigit = isDigit(prev);
1931
- const currIsDigit = isDigit(ch);
1932
- if (prevIsDigit !== currIsDigit) {
1933
- tokens.push(current);
1934
- current = ch;
1935
- continue;
1936
- }
1937
- }
1938
- if (current.length > 0 && isLower(prev) && isUpper(ch)) {
1939
- tokens.push(current);
1940
- current = ch;
1941
- continue;
1942
- }
1943
- if (current.length > 1 && isUpper(ch) === false && isUpper(prev) && !isDigit(ch)) {
1944
- const lastChar = current[current.length - 1];
1945
- if (isUpper(lastChar)) {
1946
- tokens.push(current.slice(0, -1));
1947
- current = lastChar + ch;
1948
- continue;
1949
- }
1950
- }
1951
- current += ch;
1952
- }
1953
- if (current.length > 0) {
1954
- tokens.push(current);
1955
- }
1956
- return tokens;
1957
- }
1958
- /**
1959
- * Extract searchable tokens from a file path.
1960
- * "src/core/search/symbol-tokenizer.ts" → ["src", "core", "search", "symbol", "tokenizer", "ts"]
1961
- */
1962
- static tokenizeFilePath(filePath) {
1963
- const withoutExt = filePath.replace(/\.[^.]+$/, "");
1964
- const segments = withoutExt.split(/[/\\]+/);
1965
- const tokens = [];
1966
- for (const seg of segments) {
1967
- const parts = seg.split(/[_\-.]+/).filter((s) => s.length > 0);
1968
- for (const p of parts) {
1969
- const lowered = p.toLowerCase();
1970
- if (lowered.length > 0 && !tokens.includes(lowered)) {
1971
- tokens.push(lowered);
1972
- }
1973
- }
1974
- }
1975
- return tokens;
1976
- }
1977
- /** Check if a token is a stop word */
1978
- static isStopWord(token) {
1979
- return STOP_WORDS.has(token.toLowerCase());
1980
- }
1981
- };
1982
- function isUpper(ch) {
1983
- return ch >= "A" && ch <= "Z";
1984
- }
1985
- function isLower(ch) {
1986
- return ch >= "a" && ch <= "z";
1987
- }
1988
- function isDigit(ch) {
1989
- return ch >= "0" && ch <= "9";
1990
- }
1991
-
1992
1819
  export {
1993
1820
  detectWorkspace,
1994
1821
  buildCoChangeMatrix,
1995
1822
  CoChangeReader,
1996
- SymbolTokenizer,
1997
1823
  CommunitySnapshotWriter,
1998
1824
  SqliteStorageAdapter,
1999
1825
  RevertDetector,
@@ -2007,4 +1833,4 @@ export {
2007
1833
  findCoChangedFilesForBlastRadius,
2008
1834
  ContextsViewReader
2009
1835
  };
2010
- //# sourceMappingURL=chunk-ZL36YS2V.js.map
1836
+ //# sourceMappingURL=chunk-A4URKKPP.js.map
@@ -96,7 +96,7 @@ async function loadModulesFor(projectRoot, mode, options = {}) {
96
96
  import("./plugin-discovery-XRXI3IJF.js")
97
97
  ]);
98
98
  if (resolvePackageCandidates(DOMAIN_MODULE_PACKAGE, projectRoot, options.cliAnchor).length === 0) return [];
99
- const { createModuleHost } = await import("./module-host-KNMKS62R.js");
99
+ const { createModuleHost } = await import("./module-host-KTGDODES.js");
100
100
  const { modules } = await loadCtxoModules(createModuleHost({ projectRoot, mode }), { cliAnchor: options.cliAnchor });
101
101
  return modules;
102
102
  } catch (err) {
@@ -125,4 +125,4 @@ export {
125
125
  loadModulesFor,
126
126
  runFastRoute
127
127
  };
128
- //# sourceMappingURL=chunk-SRQZDGFZ.js.map
128
+ //# sourceMappingURL=chunk-BBSJB4CG.js.map
@@ -266,4 +266,4 @@ export {
266
266
  isMarkedArtifact,
267
267
  JsonIndexReader
268
268
  };
269
- //# sourceMappingURL=chunk-X4TVFJ4C.js.map
269
+ //# sourceMappingURL=chunk-EC6INGPV.js.map
@@ -6,6 +6,197 @@ import {
6
6
  createLogger
7
7
  } from "./chunk-5DSKZJNN.js";
8
8
 
9
+ // src/core/search/symbol-tokenizer.ts
10
+ var STOP_WORDS = /* @__PURE__ */ new Set([
11
+ "get",
12
+ "set",
13
+ "is",
14
+ "has",
15
+ "can",
16
+ "do",
17
+ "the",
18
+ "a",
19
+ "an",
20
+ "of",
21
+ "to",
22
+ "in",
23
+ "for",
24
+ "on",
25
+ "with",
26
+ "at",
27
+ "by",
28
+ "from",
29
+ "as",
30
+ "into",
31
+ "not",
32
+ "no",
33
+ "or"
34
+ ]);
35
+ var DEFAULT_OPTIONS = {
36
+ includeStopWords: false,
37
+ includeOriginal: true,
38
+ includeFilePath: false
39
+ };
40
+ var SymbolTokenizer = class _SymbolTokenizer {
41
+ options;
42
+ constructor(options) {
43
+ this.options = { ...DEFAULT_OPTIONS, ...options };
44
+ }
45
+ /**
46
+ * Tokenize a symbol name into searchable tokens.
47
+ *
48
+ * Examples:
49
+ * "getCoChangeMetrics" → ["get", "co", "change", "metrics"] (or without stop words: ["co", "change", "metrics"])
50
+ * "SqliteStorageAdapter" → ["sqlite", "storage", "adapter"]
51
+ * "i_storage_port" → ["i", "storage", "port"]
52
+ * "BM25Scorer" → ["bm", "25", "scorer"]
53
+ * "DB_PASSWORD" → ["db", "password"]
54
+ */
55
+ tokenize(symbolName, filePath) {
56
+ const splitTokens = _SymbolTokenizer.splitName(symbolName);
57
+ const lowered = splitTokens.map((t) => t.toLowerCase());
58
+ const tokens = this.options.includeStopWords ? lowered : lowered.filter((t) => !STOP_WORDS.has(t));
59
+ if (this.options.includeOriginal) {
60
+ const original = symbolName.toLowerCase();
61
+ if (!tokens.includes(original)) {
62
+ tokens.unshift(original);
63
+ }
64
+ }
65
+ if (this.options.includeFilePath && filePath) {
66
+ const pathTokens = _SymbolTokenizer.tokenizeFilePath(filePath);
67
+ for (const pt of pathTokens) {
68
+ if (!tokens.includes(pt)) {
69
+ tokens.push(pt);
70
+ }
71
+ }
72
+ }
73
+ return tokens;
74
+ }
75
+ /**
76
+ * Tokenize prose (a document title or body). Words are runs of letters, digits, `_` and `-`; each word is
77
+ * split like a symbol name (camelCase, snake_case, rule IDs such as `LEND-03`) and lowercased. Stop words
78
+ * are dropped; repeats are kept, so BM25 sees the term frequency.
79
+ *
80
+ * "A member holds LoanPolicy LEND-03" → ["member", "holds", "loan", "policy", "lend", "03"]
81
+ */
82
+ tokenizeText(text) {
83
+ const tokens = [];
84
+ for (const word of text.split(/[^\p{L}\p{N}_-]+/u)) {
85
+ if (word.length === 0) continue;
86
+ for (const part of _SymbolTokenizer.splitName(word)) {
87
+ const lowered = part.toLowerCase();
88
+ if (!STOP_WORDS.has(lowered)) tokens.push(lowered);
89
+ }
90
+ }
91
+ return tokens;
92
+ }
93
+ /**
94
+ * Tokenize a search query. Queries use stop words and don't include originals.
95
+ */
96
+ tokenizeQuery(query) {
97
+ const words = query.trim().split(/\s+/);
98
+ const tokens = [];
99
+ for (const word of words) {
100
+ const split = _SymbolTokenizer.splitName(word);
101
+ for (const t of split) {
102
+ const lowered = t.toLowerCase();
103
+ if (lowered.length > 0 && !tokens.includes(lowered)) {
104
+ tokens.push(lowered);
105
+ }
106
+ }
107
+ }
108
+ return tokens;
109
+ }
110
+ /**
111
+ * Split a name into constituent words based on naming convention boundaries.
112
+ *
113
+ * Rules:
114
+ * 1. Split on underscores and hyphens (snake_case, kebab-case)
115
+ * 2. Split on camelCase/PascalCase boundaries (uppercase after lowercase)
116
+ * 3. Split acronym boundaries (e.g., "HTMLParser" → ["HTML", "Parser"])
117
+ * 4. Split on digit ↔ letter boundaries
118
+ */
119
+ static splitName(name) {
120
+ if (!name || name.length === 0) return [];
121
+ const segments = name.split(/[_\-./\\]+/).filter((s) => s.length > 0);
122
+ const tokens = [];
123
+ for (const segment of segments) {
124
+ tokens.push(..._SymbolTokenizer.splitCamelCase(segment));
125
+ }
126
+ return tokens.filter((t) => t.length > 0);
127
+ }
128
+ /**
129
+ * Split a single segment on camelCase/PascalCase/digit boundaries.
130
+ */
131
+ static splitCamelCase(segment) {
132
+ const tokens = [];
133
+ let current = "";
134
+ for (let i = 0; i < segment.length; i++) {
135
+ const ch = segment[i];
136
+ const prev = i > 0 ? segment[i - 1] : "";
137
+ if (current.length > 0) {
138
+ const prevIsDigit = isDigit(prev);
139
+ const currIsDigit = isDigit(ch);
140
+ if (prevIsDigit !== currIsDigit) {
141
+ tokens.push(current);
142
+ current = ch;
143
+ continue;
144
+ }
145
+ }
146
+ if (current.length > 0 && isLower(prev) && isUpper(ch)) {
147
+ tokens.push(current);
148
+ current = ch;
149
+ continue;
150
+ }
151
+ if (current.length > 1 && isUpper(ch) === false && isUpper(prev) && !isDigit(ch)) {
152
+ const lastChar = current[current.length - 1];
153
+ if (isUpper(lastChar)) {
154
+ tokens.push(current.slice(0, -1));
155
+ current = lastChar + ch;
156
+ continue;
157
+ }
158
+ }
159
+ current += ch;
160
+ }
161
+ if (current.length > 0) {
162
+ tokens.push(current);
163
+ }
164
+ return tokens;
165
+ }
166
+ /**
167
+ * Extract searchable tokens from a file path.
168
+ * "src/core/search/symbol-tokenizer.ts" → ["src", "core", "search", "symbol", "tokenizer", "ts"]
169
+ */
170
+ static tokenizeFilePath(filePath) {
171
+ const withoutExt = filePath.replace(/\.[^.]+$/, "");
172
+ const segments = withoutExt.split(/[/\\]+/);
173
+ const tokens = [];
174
+ for (const seg of segments) {
175
+ const parts = seg.split(/[_\-.]+/).filter((s) => s.length > 0);
176
+ for (const p of parts) {
177
+ const lowered = p.toLowerCase();
178
+ if (lowered.length > 0 && !tokens.includes(lowered)) {
179
+ tokens.push(lowered);
180
+ }
181
+ }
182
+ }
183
+ return tokens;
184
+ }
185
+ /** Check if a token is a stop word */
186
+ static isStopWord(token) {
187
+ return STOP_WORDS.has(token.toLowerCase());
188
+ }
189
+ };
190
+ function isUpper(ch) {
191
+ return ch >= "A" && ch <= "Z";
192
+ }
193
+ function isLower(ch) {
194
+ return ch >= "a" && ch <= "z";
195
+ }
196
+ function isDigit(ch) {
197
+ return ch >= "0" && ch <= "9";
198
+ }
199
+
9
200
  // src/core/co-change/co-change-window.ts
10
201
  var SINCE_PATTERN = /^(\d+)([dwmy])$/;
11
202
  var SECONDS_PER_DAY = 86400;
@@ -1022,6 +1213,7 @@ var SymbolGraph = class {
1022
1213
  };
1023
1214
 
1024
1215
  export {
1216
+ SymbolTokenizer,
1025
1217
  parseCoChangeSince,
1026
1218
  windowCutoff,
1027
1219
  selectWindow,
@@ -1046,4 +1238,4 @@ export {
1046
1238
  isDataResourceKind,
1047
1239
  SymbolGraph
1048
1240
  };
1049
- //# sourceMappingURL=chunk-7HSA5JVP.js.map
1241
+ //# sourceMappingURL=chunk-I7RPXT6L.js.map