fallow-type-aware 3.16.0 → 3.17.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/README.md CHANGED
@@ -25,6 +25,13 @@ and SHA-256 declaration guard, but Fallow owns the final decision and fix
25
25
  policy. The sidecar does not emit TypeScript compiler diagnostics as Fallow
26
26
  findings and does not implement generic typed lint rules.
27
27
 
28
+ The raw TypeScript-Go host cannot currently expose Svelte virtual-module named
29
+ exports. If source code imports or re-exports such a name and checker resolution
30
+ has no declaration target, the sidecar returns
31
+ `svelte-virtual-module-exports` instead of claiming complete evidence. Run
32
+ `svelte-check` for framework diagnostics; Fallow stays fail-closed until a
33
+ supported host seam can preserve virtual source identity and mappings.
34
+
28
35
  ## Run locally
29
36
 
30
37
  ```sh
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { assertTypescriptBackendResolvable } from "./src/backend-preflight.mjs";
3
+ import { installWindowsChildProcessPolicy } from "./src/windows-child-process.mjs";
4
+
5
+ installWindowsChildProcessPolicy();
4
6
 
5
7
  try {
8
+ const { assertTypescriptBackendResolvable } = await import("./src/backend-preflight.mjs");
6
9
  assertTypescriptBackendResolvable();
7
10
  const { run } = await import("./src/cli.mjs");
8
11
  await run({ input: process.stdin, output: process.stdout, args: process.argv.slice(2) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fallow-type-aware",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
4
4
  "description": "Optional TypeScript-Go semantic refinement sidecar for Fallow",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@codspeed/tinybench-plugin": "5.7.1",
35
- "tinybench": "6.1.2"
35
+ "tinybench": "6.1.2",
36
+ "zod": "4.4.3"
36
37
  }
37
38
  }
@@ -1,8 +1,8 @@
1
1
  // Generated from crates/api/type-aware-protocol.json. Do not edit.
2
2
  export const TYPE_AWARE_PROTOCOL = Object.freeze({
3
3
  schema_version: 1,
4
- wire_protocol_version: 6,
5
- semantic_schema_version: 2,
4
+ wire_protocol_version: 7,
5
+ semantic_schema_version: 3,
6
6
  analysis_operation: "semantic-queries",
7
7
  status_operation: "status",
8
8
  query_operations: ["symbol-use", "symbol-trace", "api-surface", "symbol-impact", "type-coupling"],
@@ -2,18 +2,107 @@ import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
+ import { SymbolFlags } from "typescript/unstable/sync";
6
+ import {
7
+ isExportDeclaration,
8
+ isImportDeclaration,
9
+ isNamedExports,
10
+ isNamedImports,
11
+ } from "typescript/unstable/ast/is";
12
+
5
13
  import { canonicalFileIdentity } from "./file-identity.mjs";
6
- import { relativePath } from "./semantic-identity.mjs";
14
+ import { projectSourceFiles, relativePath } from "./semantic-identity.mjs";
7
15
 
8
16
  const INFERRED_PROJECT = "<inferred>";
9
17
  const compareText = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
10
18
  const slash = (value) => value.split(path.sep).join("/");
11
19
 
12
- const blockingDiagnosticCount = (project) =>
13
- project.program.getConfigFileParsingDiagnostics().length +
14
- project.program.getProgramDiagnostics().length +
15
- project.program.getSyntacticDiagnostics().length +
16
- project.program.getBindDiagnostics().length;
20
+ const structuralDiagnostics = (project) => [
21
+ ...project.program.getConfigFileParsingDiagnostics(),
22
+ ...project.program.getProgramDiagnostics(),
23
+ ...project.program.getSyntacticDiagnostics(),
24
+ ...project.program.getBindDiagnostics(),
25
+ ];
26
+
27
+ const isProjectLocalDiagnostic = (project, diagnostic) => {
28
+ if (!diagnostic.fileName) return true;
29
+ const sourceFile = project.program.getSourceFile(diagnostic.fileName);
30
+ if (!sourceFile) return true;
31
+ return (
32
+ !project.program.isSourceFileDefaultLibrary(sourceFile) &&
33
+ !project.program.isSourceFileFromExternalLibrary(sourceFile)
34
+ );
35
+ };
36
+
37
+ const isSvelteSpecifier = (node) =>
38
+ typeof node.moduleSpecifier?.text === "string" && node.moduleSpecifier.text.endsWith(".svelte");
39
+
40
+ const isUnknownAlias = (checker, specifier) => {
41
+ const symbol = checker.getSymbolAtLocation(specifier.name);
42
+ if (!symbol) return true;
43
+ return checker.isUnknownSymbol(checker.getAliasedSymbol(symbol));
44
+ };
45
+
46
+ const concreteExportSymbol = (checker, symbol) => {
47
+ const target =
48
+ (symbol.flags & SymbolFlags.Alias) === 0 ? symbol : checker.getAliasedSymbol(symbol);
49
+ return !checker.isUnknownSymbol(target) && (target.declarations?.length ?? 0) > 0;
50
+ };
51
+
52
+ const hasProvableNamedExports = (project, declaration) => {
53
+ const moduleSymbol = project.checker.getSymbolAtLocation(declaration.moduleSpecifier);
54
+ if (!moduleSymbol) return false;
55
+ const hasConcreteModuleDeclaration = moduleSymbol.declarations?.some((moduleDeclaration) => {
56
+ const declarationPath =
57
+ moduleDeclaration.path ??
58
+ moduleDeclaration.fileName ??
59
+ moduleDeclaration.getSourceFile?.().fileName ??
60
+ "";
61
+ return declarationPath.endsWith(".d.svelte") || declarationPath.endsWith(".d.svelte.ts");
62
+ });
63
+ if (!hasConcreteModuleDeclaration) return false;
64
+ const namedExports = project.checker
65
+ .getExportsOfModule(moduleSymbol)
66
+ .filter((symbol) => symbol.name !== "default");
67
+ return namedExports.every((symbol) => concreteExportSymbol(project.checker, symbol));
68
+ };
69
+
70
+ const svelteDeclarationHasGap = (project, node) => {
71
+ if (!isSvelteSpecifier(node)) return false;
72
+ if (isImportDeclaration(node)) {
73
+ const bindings = node.importClause?.namedBindings;
74
+ return (
75
+ bindings !== undefined &&
76
+ isNamedImports(bindings) &&
77
+ bindings.elements.some((item) => isUnknownAlias(project.checker, item))
78
+ );
79
+ }
80
+ if (!isExportDeclaration(node)) return false;
81
+ if (!node.exportClause) return !hasProvableNamedExports(project, node);
82
+ if (!isNamedExports(node.exportClause)) return !hasProvableNamedExports(project, node);
83
+ return node.exportClause.elements.some((item) => isUnknownAlias(project.checker, item));
84
+ };
85
+
86
+ const sourceHasSvelteVirtualModuleGap = (project, sourceFile) => {
87
+ let gap = false;
88
+ const visit = (node) => {
89
+ if (gap) return;
90
+ gap = svelteDeclarationHasGap(project, node);
91
+ if (gap) return;
92
+ node.forEachChild((child) => {
93
+ visit(child);
94
+ return undefined;
95
+ });
96
+ };
97
+ visit(sourceFile);
98
+ return gap;
99
+ };
100
+
101
+ const hasSvelteVirtualModuleGap = (project) =>
102
+ projectSourceFiles(project).some(
103
+ (sourceFile) =>
104
+ sourceFile.text.includes(".svelte") && sourceHasSvelteVirtualModuleGap(project, sourceFile),
105
+ );
17
106
 
18
107
  const configPath = (root, project) => {
19
108
  const normalized = slash(project.configFileName);
@@ -173,15 +262,27 @@ const effectiveProjectConfigHash = (root, project) => {
173
262
  };
174
263
 
175
264
  export const projectState = (root, project, source) => {
176
- const diagnosticCount = blockingDiagnosticCount(project);
265
+ const diagnostics = structuralDiagnostics(project);
266
+ const localDiagnosticCount = diagnostics.filter((diagnostic) =>
267
+ isProjectLocalDiagnostic(project, diagnostic),
268
+ ).length;
269
+ const hasLocalDiagnostics = localDiagnosticCount > 0;
270
+ const svelteVirtualModuleGap = !hasLocalDiagnostics && hasSvelteVirtualModuleGap(project);
271
+ const reasonCode = hasLocalDiagnostics
272
+ ? "blocking-diagnostics"
273
+ : svelteVirtualModuleGap
274
+ ? "svelte-virtual-module-exports"
275
+ : diagnostics.length > 0
276
+ ? "blocking-diagnostics"
277
+ : null;
177
278
  return {
178
279
  project,
179
280
  config: configPath(root, project),
180
281
  effective_config_hash: effectiveProjectConfigHash(root, project),
181
282
  source,
182
- status: diagnosticCount === 0 ? "complete" : "unavailable",
183
- reason_code: diagnosticCount === 0 ? null : "blocking-diagnostics",
184
- blocking_diagnostic_count: diagnosticCount,
283
+ status: reasonCode === null ? "complete" : "unavailable",
284
+ reason_code: reasonCode,
285
+ blocking_diagnostic_count: reasonCode === "blocking-diagnostics" ? diagnostics.length : 0,
185
286
  source_file_count: project.program.getSourceFileNames().length,
186
287
  program_reused: false,
187
288
  candidate_count: 0,
package/src/semantic.mjs CHANGED
@@ -157,6 +157,10 @@ const REASON_ACTIONS = new Map([
157
157
  "blocking-diagnostics",
158
158
  "Repair structural TypeScript diagnostics in every selected project and retry.",
159
159
  ],
160
+ [
161
+ "svelte-virtual-module-exports",
162
+ "Run svelte-check for framework diagnostics. See https://docs.fallow.tools/analysis/type-aware#svelte-virtual-module-exports for supported Svelte project setup.",
163
+ ],
160
164
  [
161
165
  "unknown-entry-point",
162
166
  "Refresh the package entry points or pass project-relative source entry points.",
@@ -166,21 +170,22 @@ const DEFAULT_REASON_ACTION =
166
170
  "Narrow the query to a specific symbol, entry point, or healthy TypeScript project and retry.";
167
171
  const REASON_PRIORITY = new Map([
168
172
  ["blocking-diagnostics", 0],
169
- ["incomplete-project-coverage", 1],
170
- ["framework-contract-provenance", 2],
171
- ["ambiguous-project", 3],
172
- ["unknown-symbol", 4],
173
- ["unknown-entry-point", 5],
174
- ["decorated-declaration", 6],
175
- ["optional-contract", 7],
176
- ["accessor-pair", 8],
177
- ["overload-set", 9],
178
- ["attached-comment", 10],
179
- ["abstract-declaration", 11],
180
- ["dynamic-member-access", 12],
181
- ["virtual-dispatch", 13],
182
- ["dynamic-behavior", 14],
183
- ["evidence-limit", 15],
173
+ ["svelte-virtual-module-exports", 1],
174
+ ["incomplete-project-coverage", 2],
175
+ ["framework-contract-provenance", 3],
176
+ ["ambiguous-project", 4],
177
+ ["unknown-symbol", 5],
178
+ ["unknown-entry-point", 6],
179
+ ["decorated-declaration", 7],
180
+ ["optional-contract", 8],
181
+ ["accessor-pair", 9],
182
+ ["overload-set", 10],
183
+ ["attached-comment", 11],
184
+ ["abstract-declaration", 12],
185
+ ["dynamic-member-access", 13],
186
+ ["virtual-dispatch", 14],
187
+ ["dynamic-behavior", 15],
188
+ ["evidence-limit", 16],
184
189
  ]);
185
190
 
186
191
  const actionForReason = (reasonCode) => REASON_ACTIONS.get(reasonCode) ?? DEFAULT_REASON_ACTION;
@@ -201,6 +206,22 @@ const combineOmissions = (omissions) => {
201
206
  return [...counts].map(([reason_code, count]) => ({ reason_code, count }));
202
207
  };
203
208
 
209
+ const projectStateReason = (state) => state.reason_code ?? "blocking-diagnostics";
210
+
211
+ const unavailableProjectOmissions = (states) =>
212
+ combineOmissions(
213
+ states
214
+ .filter((state) => state.status !== "complete")
215
+ .map((state) => ({ reason_code: projectStateReason(state), count: 1 })),
216
+ );
217
+
218
+ const unavailableProjectReason = (states) => {
219
+ if (states.length === 0) return "no-project";
220
+ return (
221
+ unavailableProjectOmissions(states).toSorted(compareOmissions)[0]?.reason_code ?? "no-project"
222
+ );
223
+ };
224
+
204
225
  const resultStatus = (partial) => (partial ? "partial" : "complete");
205
226
  const resultReason = (omissions) => (omissions.length > 0 ? omissions[0].reason_code : null);
206
227
  const resultActions = (omissions) =>
@@ -681,8 +702,6 @@ const symbolResolutionError = (query, reasonCode, action) => ({
681
702
  });
682
703
 
683
704
  const isCompleteProjectState = (state) => state?.status === "complete";
684
- const selectedProjectName = (state) => state?.config ?? "the selected project";
685
-
686
705
  const owningProjectContexts = (statesByProject, absolutePath) =>
687
706
  [...statesByProject.entries()]
688
707
  .filter(([project]) => project.program.getSourceFile(absolutePath))
@@ -722,13 +741,16 @@ const selectSymbolContext = (
722
741
  }
723
742
  const completeOwners = owners.filter(({ state }) => isCompleteProjectState(state));
724
743
  if (completeOwners.length === 0) {
725
- return {
726
- ...owners[0],
727
- ...symbolResolutionError(
728
- query,
729
- "blocking-diagnostics",
730
- `Repair structural diagnostics in ${selectedProjectName(owners[0].state)} and retry.`,
744
+ const selectedOwner = [...owners].toSorted((left, right) =>
745
+ compareOmissions(
746
+ { reason_code: projectStateReason(left.state) },
747
+ { reason_code: projectStateReason(right.state) },
731
748
  ),
749
+ )[0];
750
+ const reasonCode = projectStateReason(selectedOwner.state);
751
+ return {
752
+ ...selectedOwner,
753
+ ...symbolResolutionError(query, reasonCode, actionForReason(reasonCode)),
732
754
  };
733
755
  }
734
756
  return { owners, completeOwners };
@@ -1377,6 +1399,13 @@ const signatureTypes = (project, type, anchor) =>
1377
1399
  );
1378
1400
 
1379
1401
  const checkerTypeChildren = (project, type, anchor, hasNamedDeclaration) => {
1402
+ if (type.isTypeParameter()) {
1403
+ const constraint = safeCheckerValue(
1404
+ () => project.checker.getConstraintOfTypeParameter(type),
1405
+ undefined,
1406
+ );
1407
+ return constraint ? [constraint] : [];
1408
+ }
1380
1409
  const structural = [
1381
1410
  ...(type.getTypes() ?? []),
1382
1411
  ...safeCheckerValue(() => type.getAliasTypeArguments(), []),
@@ -1547,12 +1576,9 @@ const graphProjects = (snapshot, explicitProjects) =>
1547
1576
  const readyProjectStates = (query, states) => {
1548
1577
  const ready = states.filter((state) => state.status === "complete");
1549
1578
  if (ready.length > 0) return { ready };
1579
+ const reasonCode = unavailableProjectReason(states);
1550
1580
  return {
1551
- error: unavailable(
1552
- query,
1553
- states.length === 0 ? "no-project" : "blocking-diagnostics",
1554
- "Pass a healthy tsconfig containing the package entry points and retry.",
1555
- ),
1581
+ error: unavailable(query, reasonCode, actionForReason(reasonCode)),
1556
1582
  };
1557
1583
  };
1558
1584
 
@@ -1684,7 +1710,7 @@ const analyzeApiSurface = (root, query, states, evidenceLimit) => {
1684
1710
  reason_code: "evidence-limit",
1685
1711
  count: omissionCount,
1686
1712
  },
1687
- { reason_code: "blocking-diagnostics", count: unavailableProjectCount },
1713
+ ...unavailableProjectOmissions(states),
1688
1714
  { reason_code: "unknown-entry-point", count: missingEntryPointCount },
1689
1715
  ],
1690
1716
  });
@@ -1763,6 +1789,7 @@ const analyzeGraphSemanticQuery = (root, query, graphStates, evidenceLimit) => {
1763
1789
  missingEntryPoints,
1764
1790
  publicApiGraph,
1765
1791
  readyProjectStates,
1792
+ unavailableProjectOmissions,
1766
1793
  uniqueSorted,
1767
1794
  },
1768
1795
  );
@@ -1842,6 +1869,7 @@ const recordAbstainedProjectOutcome = (state, result) => {
1842
1869
  "no-project",
1843
1870
  "ambiguous-project",
1844
1871
  "blocking-diagnostics",
1872
+ "svelte-virtual-module-exports",
1845
1873
  "unknown-symbol",
1846
1874
  "incomplete-project-coverage",
1847
1875
  ]);
@@ -118,7 +118,6 @@ export const analyzeTypeCoupling = ({ root, query, states, evidenceLimit }, serv
118
118
  const highCouplingThreshold = percentile(degrees, 0.9);
119
119
  const topContributors = topCouplingContributors(perFile);
120
120
  const cycles = query.includeCycles ? findCycles(edges) : [];
121
- const unavailableProjectCount = states.length - readiness.ready.length;
122
121
  const missingEntryPointCount = services.missingEntryPoints(query, resolvedEntryPoints);
123
122
  const nestedFileOmissionCount = nestedCouplingOmissionCount(perFile, evidenceLimit);
124
123
  const boundFile = (entry) => boundCoupledFile(entry, evidenceLimit);
@@ -159,7 +158,7 @@ export const analyzeTypeCoupling = ({ root, query, states, evidenceLimit }, serv
159
158
  Math.max(0, cycles.length - evidenceLimit) +
160
159
  nestedFileOmissionCount,
161
160
  },
162
- { reason_code: "blocking-diagnostics", count: unavailableProjectCount },
161
+ ...services.unavailableProjectOmissions(states),
163
162
  { reason_code: "unknown-entry-point", count: missingEntryPointCount },
164
163
  ],
165
164
  });
@@ -0,0 +1,25 @@
1
+ import childProcess from "node:child_process";
2
+ import { syncBuiltinESMExports } from "node:module";
3
+
4
+ const INSTALL_MARKER = Symbol.for("fallow.type-aware.windows-child-process-policy");
5
+
6
+ const hiddenOptions = (options) => ({ ...options, windowsHide: true });
7
+
8
+ /** Keep TypeScript-Go child processes hidden when the sidecar runs on Windows. */
9
+ export const installWindowsChildProcessPolicy = ({
10
+ childProcess: processApi = childProcess,
11
+ platform = process.platform,
12
+ syncBuiltinESMExports: syncExports = syncBuiltinESMExports,
13
+ } = {}) => {
14
+ if (platform !== "win32" || processApi[INSTALL_MARKER] === true) return;
15
+
16
+ const originalSpawn = processApi.spawn;
17
+ processApi.spawn = function spawnHidden(command, args, options) {
18
+ if (Array.isArray(args)) {
19
+ return originalSpawn.call(this, command, args, hiddenOptions(options));
20
+ }
21
+ return originalSpawn.call(this, command, hiddenOptions(args));
22
+ };
23
+ Object.defineProperty(processApi, INSTALL_MARKER, { value: true });
24
+ syncExports();
25
+ };