@mrclrchtr/supi-code-intelligence 5.0.0 → 6.0.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 (139) hide show
  1. package/README.md +2 -2
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  4. package/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  5. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
  6. package/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  8. package/node_modules/@mrclrchtr/supi-lsp/README.md +34 -3
  9. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  15. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  16. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +12 -1
  17. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +67 -28
  18. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +63 -6
  19. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +46 -5
  20. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +1 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +214 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +278 -79
  23. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +29 -8
  24. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +101 -19
  25. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +19 -1
  26. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +49 -3
  27. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +73 -15
  28. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +1 -1
  29. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +2 -2
  30. package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +5 -5
  31. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +3 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-extends.ts +68 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-path.ts +8 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +176 -24
  35. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +4 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +46 -10
  37. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +4 -3
  38. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +65 -17
  39. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +143 -21
  40. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +2 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +43 -1
  42. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +31 -0
  43. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  44. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  45. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  46. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
  47. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  48. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  49. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  50. package/package.json +5 -5
  51. package/src/analysis/health/diagnostics.ts +58 -126
  52. package/src/analysis/health/file-scope.ts +184 -0
  53. package/src/analysis/health/recovery.ts +8 -1
  54. package/src/analysis/readiness.ts +4 -0
  55. package/src/analysis/search/ast-scan.ts +4 -1
  56. package/src/analysis/search/pattern-analysis.ts +10 -3
  57. package/src/analysis/search/pattern.ts +6 -1
  58. package/src/config.ts +35 -1
  59. package/src/extension.ts +57 -14
  60. package/src/overview/overview-data.ts +9 -2
  61. package/src/overview/overview.ts +49 -30
  62. package/src/overview/types.ts +2 -0
  63. package/src/session/find-types.ts +2 -2
  64. package/src/session/find-workflow.ts +3 -2
  65. package/src/session/health-refresh.ts +76 -49
  66. package/src/session/health-types.ts +7 -1
  67. package/src/session/health-workflow.ts +35 -18
  68. package/src/session/input/workflows.ts +2 -2
  69. package/src/session/inspect-workflow.ts +2 -1
  70. package/src/session/orientation/collect.ts +2 -2
  71. package/src/session/orientation-types.ts +2 -0
  72. package/src/session/orientation-workflow.ts +8 -2
  73. package/src/session/refactor-workflow.ts +2 -1
  74. package/src/session/session.ts +25 -1
  75. package/src/substrate/lsp/maintenance.ts +105 -23
  76. package/src/substrate/lsp/recovery.ts +11 -5
  77. package/src/tool/code_find/execute.ts +23 -0
  78. package/src/tool/code_find/guidance.ts +8 -0
  79. package/src/tool/{find → code_find}/render.ts +1 -1
  80. package/src/tool/{result/find.ts → code_find/result.ts} +36 -11
  81. package/src/tool/code_find/spec.ts +37 -0
  82. package/src/tool/code_graph/execute.ts +24 -0
  83. package/src/tool/code_graph/guidance.ts +6 -0
  84. package/src/tool/{graph → code_graph}/markdown.ts +1 -1
  85. package/src/tool/{result/graph.ts → code_graph/result.ts} +69 -4
  86. package/src/tool/code_graph/spec.ts +39 -0
  87. package/src/tool/{health → code_health}/execute.ts +2 -18
  88. package/src/tool/code_health/guidance.ts +8 -0
  89. package/src/tool/{health → code_health}/markdown.ts +68 -18
  90. package/src/tool/{result/health.ts → code_health/result.ts} +29 -4
  91. package/src/tool/code_health/spec.ts +41 -0
  92. package/src/tool/code_inspect/execute.ts +20 -0
  93. package/src/tool/code_inspect/guidance.ts +6 -0
  94. package/src/tool/{inspect → code_inspect}/markdown.ts +1 -1
  95. package/src/tool/{result/inspect.ts → code_inspect/result.ts} +32 -3
  96. package/src/tool/code_inspect/spec.ts +25 -0
  97. package/src/tool/code_orientation/execute.ts +25 -0
  98. package/src/tool/code_orientation/guidance.ts +9 -0
  99. package/src/tool/{orientation → code_orientation}/markdown.ts +1 -1
  100. package/src/tool/{result/orientation.ts → code_orientation/result.ts} +67 -3
  101. package/src/tool/code_orientation/spec.ts +25 -0
  102. package/src/tool/code_refactor_apply/execute.ts +17 -0
  103. package/src/tool/code_refactor_apply/guidance.ts +5 -0
  104. package/src/tool/{refactor-apply/execute.ts → code_refactor_apply/result.ts} +5 -11
  105. package/src/tool/code_refactor_apply/spec.ts +26 -0
  106. package/src/tool/code_refactor_plan/execute.ts +27 -0
  107. package/src/tool/code_refactor_plan/guidance.ts +8 -0
  108. package/src/tool/{refactor-plan/execute.ts → code_refactor_plan/result.ts} +9 -22
  109. package/src/tool/code_refactor_plan/spec.ts +25 -0
  110. package/src/tool/code_resolve/execute.ts +19 -0
  111. package/src/tool/code_resolve/guidance.ts +8 -0
  112. package/src/tool/{resolve → code_resolve}/markdown.ts +1 -1
  113. package/src/tool/{result/resolve.ts → code_resolve/result.ts} +37 -24
  114. package/src/tool/code_resolve/spec.ts +25 -0
  115. package/src/tool/guidance.ts +71 -52
  116. package/src/tool/infra/truncate.ts +40 -0
  117. package/src/tool/{refactor-plan/markdown.ts → refactor-markdown.ts} +4 -7
  118. package/src/tool/register.ts +9 -97
  119. package/src/tool/result/refactor.ts +1 -4
  120. package/src/tool/schemas.ts +26 -157
  121. package/src/tool/specs.ts +40 -126
  122. package/src/tool/find/execute.ts +0 -43
  123. package/src/tool/graph/execute.ts +0 -82
  124. package/src/tool/inspect/execute.ts +0 -40
  125. package/src/tool/orientation/execute.ts +0 -80
  126. package/src/tool/resolve/execute.ts +0 -53
  127. /package/src/tool/{find → code_find}/ast-kinds.ts +0 -0
  128. /package/src/tool/{find → code_find}/markdown.ts +0 -0
  129. /package/src/tool/{find → code_find}/modes.ts +0 -0
  130. /package/src/tool/{find → code_find}/tui.ts +0 -0
  131. /package/src/tool/{graph → code_graph}/tui.ts +0 -0
  132. /package/src/tool/{health → code_health}/refresh-status.ts +0 -0
  133. /package/src/tool/{health → code_health}/semantic-state.ts +0 -0
  134. /package/src/tool/{health → code_health}/tui.ts +0 -0
  135. /package/src/tool/{inspect → code_inspect}/tui.ts +0 -0
  136. /package/src/tool/{orientation → code_orientation}/tui.ts +0 -0
  137. /package/src/tool/{refactor-apply → code_refactor_apply}/tui.ts +0 -0
  138. /package/src/tool/{refactor-plan → code_refactor_plan}/tui.ts +0 -0
  139. /package/src/tool/{resolve → code_resolve}/tui.ts +0 -0
@@ -362,7 +362,7 @@ function recordRequestTiming(
362
362
  method: observation.method,
363
363
  methodClass: observation.methodClass,
364
364
  outcome: observation.outcome,
365
- ...(identity.server !== undefined ? { server: identity.server } : {}),
365
+ ...(identity.server !== undefined ? { server: truncateIdentity(identity.server) } : {}),
366
366
  ...(observation.errorCode !== undefined ? { errorCode: observation.errorCode } : {}),
367
367
  },
368
368
  }),
@@ -63,12 +63,12 @@ export const CLIENT_CAPABILITIES: ClientCapabilities = {
63
63
  workDoneProgress: true,
64
64
  },
65
65
  workspace: {
66
- workspaceFolders: false,
66
+ workspaceFolders: true,
67
67
  workspaceEdit: {
68
68
  documentChanges: true,
69
69
  },
70
70
  diagnostics: {
71
- refreshSupport: false,
71
+ refreshSupport: true,
72
72
  },
73
73
  },
74
74
  };
@@ -125,15 +125,15 @@ function mergeSingleServer(
125
125
  override: Partial<ServerConfig>,
126
126
  ): ServerConfig | null {
127
127
  if (!base) {
128
- // New custom language must have all required fields
128
+ // A custom server needs a command and at least one file extension.
129
129
  if (
130
- override.command &&
130
+ typeof override.command === "string" &&
131
+ override.command.length > 0 &&
131
132
  Array.isArray(override.fileTypes) &&
132
133
  override.fileTypes.length > 0 &&
133
- Array.isArray(override.rootMarkers) &&
134
- override.rootMarkers.length > 0
134
+ (override.rootMarkers === undefined || Array.isArray(override.rootMarkers))
135
135
  ) {
136
- return override as ServerConfig;
136
+ return { ...override, rootMarkers: override.rootMarkers ?? [] } as ServerConfig;
137
137
  }
138
138
  return null;
139
139
  }
@@ -5,8 +5,11 @@ export interface ServerConfig {
5
5
  command: string;
6
6
  args?: string[];
7
7
  fileTypes: string[];
8
+ /** Files that identify the project root. An empty list uses the session root. */
8
9
  rootMarkers: string[];
9
10
  enabled?: boolean;
11
+ /** Environment values added when the server process starts. */
12
+ env?: Record<string, string>;
10
13
  initializationOptions?: unknown;
11
14
  /** Maximum time to wait for a single $/progress cycle, in ms. Default: 10_000. */
12
15
  readinessTimeoutMs?: number;
@@ -0,0 +1,68 @@
1
+ import * as path from "node:path";
2
+ import ts from "typescript";
3
+ import { normalizeTsconfigPath as normalizePath } from "./tsconfig-path.ts";
4
+
5
+ /**
6
+ * Collect recognized local project configs declared by `extends`, including
7
+ * transitive dependencies and paths that do not exist yet.
8
+ */
9
+ export function collectExtendedProjectConfigs(configPath: string): Set<string> {
10
+ const dependencies = new Set<string>();
11
+ const visited = new Set<string>();
12
+
13
+ const visit = (currentConfigPath: string): void => {
14
+ const normalizedConfigPath = normalizePath(currentConfigPath);
15
+ if (visited.has(normalizedConfigPath)) return;
16
+ visited.add(normalizedConfigPath);
17
+
18
+ const config = ts.readConfigFile(currentConfigPath, ts.sys.readFile).config;
19
+ if (!config || typeof config !== "object") return;
20
+
21
+ const extendsValues =
22
+ typeof config.extends === "string"
23
+ ? [config.extends]
24
+ : Array.isArray(config.extends)
25
+ ? config.extends.filter((value: unknown): value is string => typeof value === "string")
26
+ : [];
27
+
28
+ for (const extendsValue of extendsValues) {
29
+ const extendedConfigPath = resolveLocalExtendedConfigPath(currentConfigPath, extendsValue);
30
+ if (!extendedConfigPath) continue;
31
+
32
+ const normalizedExtendedConfigPath = normalizePath(extendedConfigPath);
33
+ dependencies.add(normalizedExtendedConfigPath);
34
+ visit(extendedConfigPath);
35
+ }
36
+ };
37
+
38
+ visit(configPath);
39
+ return dependencies;
40
+ }
41
+
42
+ /**
43
+ * Resolve a local project-config extends entry.
44
+ *
45
+ * Package names are not local paths and are deliberately ignored. TypeScript
46
+ * uses the `.json` suffix for extensionless local config references. Only
47
+ * recognized project-config names are tracked; arbitrary local JSON files may
48
+ * still be parsed by TypeScript but are outside this invalidation contract.
49
+ */
50
+ function resolveLocalExtendedConfigPath(configPath: string, extendsValue: string): string | null {
51
+ if (!path.isAbsolute(extendsValue) && !/^\.{1,2}(?:[\\/]|$)/.test(extendsValue)) {
52
+ return null;
53
+ }
54
+
55
+ const resolved = path.resolve(path.dirname(configPath), extendsValue);
56
+ const candidate =
57
+ path.extname(resolved).toLowerCase() === ".json" ? resolved : `${resolved}.json`;
58
+ return isProjectConfigFileName(path.basename(candidate)) ? candidate : null;
59
+ }
60
+
61
+ /** Whether a file name is a tsconfig.json, jsconfig.json, or tsconfig.*.json name. */
62
+ export function isProjectConfigFileName(name: string): boolean {
63
+ return (
64
+ name === "tsconfig.json" ||
65
+ name === "jsconfig.json" ||
66
+ (name.startsWith("tsconfig.") && name.endsWith(".json"))
67
+ );
68
+ }
@@ -0,0 +1,8 @@
1
+ import * as path from "node:path";
2
+ import ts from "typescript";
3
+
4
+ /** Normalize a path for tsconfig cache keys and comparisons. */
5
+ export function normalizeTsconfigPath(target: string): string {
6
+ const resolved = path.resolve(target).replaceAll("\\", "/");
7
+ return ts.sys.useCaseSensitiveFileNames ? resolved : resolved.toLowerCase();
8
+ }
@@ -7,6 +7,10 @@
7
7
 
8
8
  import * as path from "node:path";
9
9
  import ts from "typescript";
10
+ import { collectExtendedProjectConfigs } from "./tsconfig-extends.ts";
11
+ import { normalizeTsconfigPath as normalizePath } from "./tsconfig-path.ts";
12
+
13
+ export { isProjectConfigFileName } from "./tsconfig-extends.ts";
10
14
 
11
15
  interface ParsedProjectConfig {
12
16
  configPath: string;
@@ -19,8 +23,39 @@ interface ParsedProjectConfig {
19
23
  usesDefaultInclude: boolean;
20
24
  }
21
25
 
26
+ interface ParsedProjectConfigCacheEntry {
27
+ parsed: ParsedProjectConfig | null;
28
+ dependencies: Set<string>;
29
+ }
30
+
31
+ /** Whether a file is inside the compilation scope of its nearest project config. */
32
+ export type FileScopeStatus = "included" | "excluded" | "no-config" | "out-of-tree";
33
+
34
+ /**
35
+ * The mechanism that produced a file scope decision.
36
+ *
37
+ * `extension` covers unsupported file extensions (checked before any pattern).
38
+ */
39
+ export type ScopeDecisionBasis =
40
+ | "fileNames"
41
+ | "explicit"
42
+ | "include-pattern"
43
+ | "default-include"
44
+ | "exclude-pattern"
45
+ | "extension";
46
+
47
+ /** An explainable tsconfig scope decision for one file. */
48
+ export interface FileScopeDecision {
49
+ status: FileScopeStatus;
50
+ /** Decision mechanism; null when no decision applies (no-config, out-of-tree). */
51
+ basis: ScopeDecisionBasis | null;
52
+ /** Absolute path of the config that decided the file, when one exists. */
53
+ configPath: string | null;
54
+ caseSensitiveFileNames: boolean;
55
+ }
56
+
22
57
  const nearestConfigCache = new Map<string, string | null>();
23
- const parsedConfigCache = new Map<string, ParsedProjectConfig | null>();
58
+ const parsedConfigCache = new Map<string, ParsedProjectConfigCacheEntry>();
24
59
 
25
60
  const tsInternal = ts as typeof ts & {
26
61
  getFileMatcherPatterns?: (
@@ -47,14 +82,63 @@ const tsInternal = ts as typeof ts & {
47
82
  * @returns `true` if the file is excluded from compilation scope
48
83
  */
49
84
  export function isFileExcludedByTsconfig(filePath: string, cwd: string): boolean {
85
+ // Legacy filter semantics: only a resolved "excluded" decision filters a
86
+ // file. Out-of-tree and no-config files stay unfiltered; the decision API
87
+ // still reports those statuses honestly for consumers that want them.
88
+ return getFileScopeDecision(filePath, cwd).status === "excluded";
89
+ }
90
+
91
+ /**
92
+ * Compute the explainable tsconfig scope decision for one file.
93
+ *
94
+ * The decision carries the mechanism that produced it (the scope decision
95
+ * basis) so consumers can report why a file is inside or outside the nearest
96
+ * project config without re-deriving the scope logic.
97
+ */
98
+ export function getFileScopeDecision(filePath: string, cwd: string): FileScopeDecision {
99
+ const caseSensitiveFileNames = ts.sys.useCaseSensitiveFileNames;
50
100
  const absolutePath = path.resolve(cwd, filePath);
101
+ if (isOutOfTree(cwd, absolutePath)) {
102
+ return {
103
+ status: "out-of-tree",
104
+ basis: null,
105
+ configPath: null,
106
+ caseSensitiveFileNames,
107
+ };
108
+ }
109
+
51
110
  const configPath = findNearestProjectConfig(path.dirname(absolutePath), cwd);
52
- if (!configPath) return false;
111
+ if (!configPath) {
112
+ return {
113
+ status: "no-config",
114
+ basis: null,
115
+ configPath: null,
116
+ caseSensitiveFileNames,
117
+ };
118
+ }
53
119
 
54
120
  const parsed = parseProjectConfig(configPath);
55
- if (!parsed) return false;
121
+ if (!parsed) {
122
+ return {
123
+ status: "no-config",
124
+ basis: null,
125
+ configPath,
126
+ caseSensitiveFileNames,
127
+ };
128
+ }
56
129
 
57
- return !isFileInProjectScope(parsed, absolutePath);
130
+ const decision = decideFileScope(parsed, absolutePath);
131
+ return {
132
+ status: decision.included ? "included" : "excluded",
133
+ basis: decision.basis,
134
+ configPath: parsed.configPath,
135
+ caseSensitiveFileNames,
136
+ };
137
+ }
138
+
139
+ function isOutOfTree(cwd: string, absolutePath: string): boolean {
140
+ const relative = path.relative(path.resolve(cwd), absolutePath);
141
+ return relative.startsWith(`..${path.sep}`) || relative === "..";
58
142
  }
59
143
 
60
144
  /**
@@ -104,11 +188,15 @@ function getLocalProjectConfig(directory: string): string | null {
104
188
  function parseProjectConfig(configPath: string): ParsedProjectConfig | null {
105
189
  const normalizedConfigPath = normalizePath(configPath);
106
190
  const cached = parsedConfigCache.get(normalizedConfigPath);
107
- if (cached !== undefined) return cached;
191
+ if (cached !== undefined) return cached.parsed;
108
192
 
193
+ const cacheEntry: ParsedProjectConfigCacheEntry = {
194
+ parsed: null,
195
+ dependencies: collectExtendedProjectConfigs(configPath),
196
+ };
109
197
  const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, createParseConfigHost());
110
198
  if (!parsed) {
111
- parsedConfigCache.set(normalizedConfigPath, null);
199
+ parsedConfigCache.set(normalizedConfigPath, cacheEntry);
112
200
  return null;
113
201
  }
114
202
 
@@ -119,7 +207,10 @@ function parseProjectConfig(configPath: string): ParsedProjectConfig | null {
119
207
  configDir,
120
208
  parsed.raw.include,
121
209
  parsed.raw.exclude,
122
- usesDefaultInclude,
210
+ {
211
+ useDefaultInclude: usesDefaultInclude,
212
+ useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
213
+ },
123
214
  );
124
215
  const supportedExtensions = new Set(getSupportedExtensions(parsed.options));
125
216
  if (parsed.options.resolveJsonModule) supportedExtensions.add(".json");
@@ -134,7 +225,8 @@ function parseProjectConfig(configPath: string): ParsedProjectConfig | null {
134
225
  supportedExtensions,
135
226
  usesDefaultInclude,
136
227
  } satisfies ParsedProjectConfig;
137
- parsedConfigCache.set(normalizedConfigPath, result);
228
+ cacheEntry.parsed = result;
229
+ parsedConfigCache.set(normalizedConfigPath, cacheEntry);
138
230
  return result;
139
231
  }
140
232
 
@@ -147,15 +239,21 @@ function extractExplicitFiles(rawFiles: unknown, configDir: string): Set<string>
147
239
  );
148
240
  }
149
241
 
242
+ interface FileMatcherOptions {
243
+ useDefaultInclude: boolean;
244
+ useCaseSensitiveFileNames: boolean;
245
+ }
246
+
150
247
  function createFileMatchers(
151
248
  configDir: string,
152
249
  rawInclude: unknown,
153
250
  rawExclude: unknown,
154
- useDefaultInclude: boolean,
251
+ options: FileMatcherOptions,
155
252
  ): {
156
253
  includeFilePattern: RegExp | null;
157
254
  excludePattern: RegExp | null;
158
255
  } {
256
+ const { useDefaultInclude, useCaseSensitiveFileNames } = options;
159
257
  const includeSpecs = Array.isArray(rawInclude)
160
258
  ? rawInclude.filter((entry): entry is string => typeof entry === "string")
161
259
  : undefined;
@@ -167,29 +265,45 @@ function createFileMatchers(
167
265
  excludeSpecs,
168
266
  useDefaultInclude ? ["**/*"] : includeSpecs,
169
267
  );
268
+ // normalizePath() lowercases paths when the filesystem is case-insensitive,
269
+ // so the include/exclude regexes must match case-insensitively too. Otherwise
270
+ // files created after the parse (absent from fileNames, tested against the
271
+ // include pattern) are wrongly declared excluded.
272
+ const regexFlags = useCaseSensitiveFileNames ? undefined : "i";
170
273
 
171
274
  return {
172
275
  includeFilePattern: matcherPatterns.includeFilePattern
173
- ? new RegExp(matcherPatterns.includeFilePattern)
276
+ ? new RegExp(matcherPatterns.includeFilePattern, regexFlags)
174
277
  : null,
175
278
  excludePattern: matcherPatterns.excludePattern
176
- ? new RegExp(matcherPatterns.excludePattern)
279
+ ? new RegExp(matcherPatterns.excludePattern, regexFlags)
177
280
  : null,
178
281
  };
179
282
  }
180
283
 
181
- function isFileInProjectScope(parsed: ParsedProjectConfig, absolutePath: string): boolean {
284
+ function decideFileScope(
285
+ parsed: ParsedProjectConfig,
286
+ absolutePath: string,
287
+ ): { included: boolean; basis: ScopeDecisionBasis } {
182
288
  const normalizedPath = normalizePath(absolutePath);
183
- if (parsed.fileNames.has(normalizedPath)) return true;
289
+ if (parsed.fileNames.has(normalizedPath)) return { included: true, basis: "fileNames" };
184
290
 
185
291
  const extension = path.extname(absolutePath).toLowerCase();
186
- if (!parsed.supportedExtensions.has(extension)) return false;
292
+ if (!parsed.supportedExtensions.has(extension)) return { included: false, basis: "extension" };
187
293
 
188
- if (parsed.explicitFiles) return parsed.explicitFiles.has(normalizedPath);
189
- if (!isWithinOrEqual(parsed.configDir, absolutePath)) return false;
190
- if (parsed.excludePattern?.test(normalizedPath)) return false;
191
- if (parsed.usesDefaultInclude) return true;
192
- return parsed.includeFilePattern ? parsed.includeFilePattern.test(normalizedPath) : false;
294
+ if (parsed.explicitFiles) {
295
+ return { included: parsed.explicitFiles.has(normalizedPath), basis: "explicit" };
296
+ }
297
+ // Include patterns are rooted at the config directory; a file outside it can
298
+ // only be in scope through the parse-time file set or an explicit files list.
299
+ if (!isWithinOrEqual(parsed.configDir, absolutePath)) {
300
+ return { included: false, basis: "include-pattern" };
301
+ }
302
+ if (parsed.excludePattern?.test(normalizedPath))
303
+ return { included: false, basis: "exclude-pattern" };
304
+ if (parsed.usesDefaultInclude) return { included: true, basis: "default-include" };
305
+ if (!parsed.includeFilePattern) return { included: false, basis: "include-pattern" };
306
+ return { included: parsed.includeFilePattern.test(normalizedPath), basis: "include-pattern" };
193
307
  }
194
308
 
195
309
  function getSupportedExtensions(options: ts.CompilerOptions): string[] {
@@ -229,11 +343,6 @@ function isWithinOrEqual(root: string, target: string): boolean {
229
343
  return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== "..");
230
344
  }
231
345
 
232
- function normalizePath(target: string): string {
233
- const resolved = path.resolve(target).replaceAll("\\", "/");
234
- return ts.sys.useCaseSensitiveFileNames ? resolved : resolved.toLowerCase();
235
- }
236
-
237
346
  /**
238
347
  * Clear cached nearest-config lookups and parsed project config state.
239
348
  * Useful for testing and after workspace file changes.
@@ -242,3 +351,46 @@ export function clearTsconfigCache(): void {
242
351
  nearestConfigCache.clear();
243
352
  parsedConfigCache.clear();
244
353
  }
354
+
355
+ /**
356
+ * Invalidate cached state for one config file that changed, was created, or
357
+ * was deleted.
358
+ *
359
+ * Removes the changed config and every cached root config that depends on it,
360
+ * so the next decision re-reads current config data. The wholesale
361
+ * {@link clearTsconfigCache} stays for lifecycle events; change signals use
362
+ * this targeted invalidation instead.
363
+ */
364
+ export function invalidateTsconfigCacheForConfig(configPath: string): void {
365
+ const normalizedConfigPath = normalizePath(configPath);
366
+ const invalidated = new Set([normalizedConfigPath]);
367
+
368
+ for (const [cachedConfigPath, cacheEntry] of parsedConfigCache) {
369
+ if (cacheEntry.dependencies.has(normalizedConfigPath)) invalidated.add(cachedConfigPath);
370
+ }
371
+
372
+ for (const cachedConfigPath of invalidated) {
373
+ parsedConfigCache.delete(cachedConfigPath);
374
+ }
375
+
376
+ for (const [key, value] of nearestConfigCache) {
377
+ if (value !== null && invalidated.has(normalizePath(value))) nearestConfigCache.delete(key);
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Invalidate nearest-config lookups that found nothing under `dir`.
383
+ *
384
+ * Used when a config is created: directories below the new config that
385
+ * previously resolved to no config may now resolve to it.
386
+ */
387
+ export function invalidateTsconfigCacheForConfigDir(dir: string): void {
388
+ const normalizedDir = normalizePath(dir);
389
+ for (const [key, value] of nearestConfigCache) {
390
+ if (value !== null) continue;
391
+ const keyDir = key.split("::", 1)[0];
392
+ if (keyDir === normalizedDir || keyDir.startsWith(`${normalizedDir}/`)) {
393
+ nearestConfigCache.delete(key);
394
+ }
395
+ }
396
+ }
@@ -1,3 +1,7 @@
1
+ /** Reason for a partial or unavailable single-file result when the push stays tentative. */
2
+ export const TENTATIVE_PUSH_UNAVAILABLE_REASON =
3
+ "The current push publication is tentative: a later diagnostic republish for the same document synchronization is required.";
4
+
1
5
  /** A final evidence state for one tracked document. */
2
6
  export type DiagnosticEvidenceStatus = "confirmed" | "unconfirmed" | "failed" | "removed";
3
7
 
@@ -1,20 +1,32 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { FileChangeType, type FileEvent } from "../config/types.ts";
4
- import { fileToUri } from "../utils.ts";
4
+ import { fileToUri, uriToFile } from "../utils.ts";
5
5
 
6
6
  const IGNORED_DIRECTORIES = new Set(["node_modules", ".pnpm", ".git", "dist", "coverage"]);
7
7
  const ROOT_LOCKFILES = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
8
8
 
9
+ export interface WorkspaceSentinelScanOptions {
10
+ /**
11
+ * Also track every regular source file (not just sentinels). The workspace
12
+ * refresh path uses this to discover files created since the last refresh;
13
+ * the diff then reports them as `sourceChanges` so the caller can pull them.
14
+ */
15
+ includeSourceFiles?: boolean;
16
+ }
17
+
9
18
  /** Build a fresh snapshot of workspace sentinel files and their mtimes. */
10
- export function scanWorkspaceSentinels(cwd: string): Map<string, number> {
19
+ export function scanWorkspaceSentinels(
20
+ cwd: string,
21
+ options: WorkspaceSentinelScanOptions = {},
22
+ ): Map<string, number> {
11
23
  const resolvedCwd = path.resolve(cwd);
12
24
  const snapshot = new Map<string, number>();
13
25
 
14
26
  if (!fs.existsSync(resolvedCwd)) return snapshot;
15
27
 
16
28
  try {
17
- walkWorkspace(resolvedCwd, resolvedCwd, snapshot);
29
+ walkWorkspace(resolvedCwd, resolvedCwd, snapshot, options.includeSourceFiles ?? false);
18
30
  } catch {
19
31
  return snapshot;
20
32
  }
@@ -48,15 +60,34 @@ export function diffWorkspaceSentinelSnapshot(
48
60
  return changes.sort((a, b) => a.uri.localeCompare(b.uri));
49
61
  }
50
62
 
51
- /** Refresh a previous snapshot and return the new snapshot plus change events. */
63
+ /**
64
+ * Refresh a previous snapshot and return the new snapshot plus change events.
65
+ *
66
+ * When `includeSourceFiles` is set, the returned `changes` list keeps
67
+ * sentinel-file events only; source-file events are returned separately in
68
+ * `sourceChanges` so the caller can act on them without interpreting every
69
+ * regular file edit as a config-level workspace change.
70
+ */
52
71
  export function syncWorkspaceSentinelSnapshot(
53
72
  cwd: string,
54
73
  previous: Map<string, number>,
55
- ): { snapshot: Map<string, number>; changes: FileEvent[] } {
56
- const snapshot = scanWorkspaceSentinels(cwd);
74
+ options: WorkspaceSentinelScanOptions = {},
75
+ ): { snapshot: Map<string, number>; changes: FileEvent[]; sourceChanges: FileEvent[] } {
76
+ const snapshot = scanWorkspaceSentinels(cwd, options);
77
+ const changes = diffWorkspaceSentinelSnapshot(previous, snapshot);
78
+ if (!options.includeSourceFiles) return { snapshot, changes, sourceChanges: [] };
79
+
80
+ const sourceChanges = changes.filter((change) => {
81
+ const filePath = uriToFile(change.uri);
82
+ return !isWorkspaceSentinelPath(filePath, cwd);
83
+ });
57
84
  return {
58
85
  snapshot,
59
- changes: diffWorkspaceSentinelSnapshot(previous, snapshot),
86
+ changes: changes.filter((change) => {
87
+ const filePath = uriToFile(change.uri);
88
+ return isWorkspaceSentinelPath(filePath, cwd);
89
+ }),
90
+ sourceChanges,
60
91
  };
61
92
  }
62
93
 
@@ -66,7 +97,12 @@ export function isWorkspaceRecoveryTrigger(filePath: string, cwd: string): boole
66
97
  return isWorkspaceSentinelPath(path.resolve(root, filePath), root);
67
98
  }
68
99
 
69
- function walkWorkspace(root: string, directory: string, snapshot: Map<string, number>): void {
100
+ function walkWorkspace(
101
+ root: string,
102
+ directory: string,
103
+ snapshot: Map<string, number>,
104
+ includeSourceFiles: boolean,
105
+ ): void {
70
106
  let entries: fs.Dirent[];
71
107
  try {
72
108
  entries = fs.readdirSync(directory, { withFileTypes: true });
@@ -80,14 +116,14 @@ function walkWorkspace(root: string, directory: string, snapshot: Map<string, nu
80
116
  for (const entry of entries) {
81
117
  if (entry.isDirectory()) {
82
118
  if (IGNORED_DIRECTORIES.has(entry.name)) continue;
83
- walkWorkspace(root, path.join(directory, entry.name), snapshot);
119
+ walkWorkspace(root, path.join(directory, entry.name), snapshot, includeSourceFiles);
84
120
  continue;
85
121
  }
86
122
 
87
123
  if (!entry.isFile()) continue;
88
124
 
89
125
  const filePath = path.join(directory, entry.name);
90
- if (!isWorkspaceSentinelPath(filePath, root)) continue;
126
+ if (!includeSourceFiles && !isWorkspaceSentinelPath(filePath, root)) continue;
91
127
  try {
92
128
  snapshot.set(filePath, fs.statSync(filePath).mtimeMs);
93
129
  } catch {
@@ -5,6 +5,7 @@ import {
5
5
  mapCodeQueryResult,
6
6
  } from "@mrclrchtr/supi-code-runtime/api";
7
7
  import type { LspClient } from "../client/client.ts";
8
+ import type { DiagnosticEntry } from "../client/client-document-state.ts";
8
9
  import type { Diagnostic } from "../config/types.ts";
9
10
  import { relativeFilePathFromUri } from "../diagnostics/diagnostic-summary.ts";
10
11
  import { shouldIgnoreLspPath } from "../summary.ts";
@@ -27,15 +28,15 @@ export async function syncClientFileAndGetDiagnostics(
27
28
  }
28
29
 
29
30
  export function collectOutstandingDiagnosticsDetailed(
30
- clients: Iterable<Pick<LspClient, "getAllDiagnostics">>,
31
+ clientEntries: Iterable<ReadonlyArray<DiagnosticEntry>>,
31
32
  cwd: string,
32
33
  excludePatterns: string[],
33
34
  maxSeverity: number,
34
35
  ): Array<{ file: string; diagnostics: Diagnostic[] }> {
35
36
  const fileDiags = new Map<string, Diagnostic[]>();
36
37
 
37
- for (const client of clients) {
38
- for (const entry of client.getAllDiagnostics()) {
38
+ for (const entries of clientEntries) {
39
+ for (const entry of entries) {
39
40
  const file = relativeFilePathFromUri(entry.uri, cwd);
40
41
  if (shouldIgnoreLspPath(file, cwd)) continue;
41
42
  if (isExcludedByPattern(file, excludePatterns)) continue;