mcpField
C
callidescope-trace

callidescope-trace

github.com/organizzolini开发0277 次安装今天更新
代码仓库

关于 callidescope-trace

callidescope-trace 是分析 TypeScript 工作区调用图并解读报告的技能,涵盖依赖注入调用、工作区深度与广度结果,以及单个可调用项的关联路径查询。

主要功能

  1. 01跟踪依赖注入中的调用
  2. 02按项目报告深度与广度结果
  3. 03查询直接调用者和被调用者
  4. 04展示调用者与被调用者路径
  5. 05将调用栈合并渲染为 Mermaid

适用场景

  1. 01查找超过配置深度限制的调用栈
  2. 02重命名前检查可调用项的调用者
  3. 03为职责拆分识别切分位置
  4. 04评估是否适合内联或折叠某一层

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/organizzolini/codebase --skill callidescope-trace

安装后在 Claude Code 中输入/callidescope-trace确认已加载。

详细介绍

运行 callidescope 并解读结果

Callidescope 会为一个 TypeScript 工作区构建一张调用图,并衡量其结构。它会沿着注入的依赖项追踪调用——从 this.someService.load() 跳转到 SomeService.load——这是逐文件分析工具无法看到的边,也是框架代码库中大部分控制流实际所在之处。

三个命令,分别回答三个不同的问题:

npx callidescope --config configuration/callidescope.config.ts                                        # the whole workspace
npx callidescope depth --config configuration/callidescope.config.ts --addresses src/foo.service.ts#FooService.bar    # one callable, vertically
npx callidescope breadth --config configuration/callidescope.config.ts --addresses src/foo.service.ts#FooService.bar  # one callable, horizontally

这三个命令都必须指定 --config,用于指明工作区配置文件:工具不会搜索配置文件,也不会使用默认值,因此省略该参数时会拒绝运行,并指出缺少此标志。被追踪项目自身的 callidescope.config.ts 则是另一回事:该文件必须存在且完整,否则在追踪任何内容之前就会拒绝运行——请参阅 callidescope-configure 技能。运行会以什么为门槛,以及会写入什么,都是可选的,且都在该技能中说明;本文则讲如何解读结果。

callidescope:整个工作区

报告两类发现,均按项目分别列出。

深层调用栈。 每个入口点下最深的一条路径,且仅当它超过 limits.maximumDepth 时才报告。每个入口点只会构建一条路径,因此宽图的开销不会高于窄图。

广度。 一个可调用项直接调用多少个可调用项。始终报告;仅当项目声明了 limits.maximumBreadth 时才作为门槛进行检查,这是唯一没有任何默认值的限制。

每项发现都会附带用于衡量它的限制值,因为该限制是关于调用栈根节点所属项目的事实,而不是关于本次运行的事实。一份报告中经常会有多个不同数值;两个项目中完全相同的调用栈,可能在一个项目中构成发现,而在另一个项目中不报告,代码本身却没有任何差异。

阅读调用栈

Stack #1 | 🚨 [DEPTH ≥ 10 > 6] (decorated-method)
🚀 SomeCommand.run(options: SomeOptions): Promise<void> [.../some.command.ts:220]
   ↳ Measure the repository and write every configured output.
  └─> SomeService.measure(args: MeasureArguments): Result [.../some.service.ts:115]
     ↳ Measure aggregated statistics for the provided directory.
  • ≥ 10 表示下限,而非测量值。 路径中的某处无法继续追踪——例如通过参数调用的回调、计算得到的成员名称——运行结果会明确说明这一点,而不会悄悄少报。实际深度至少为 10。这不是缺陷,也不是不可信的数值:若限制为 6,那么 ≥ 10 确实意味着超限。摘要中的 Unfollowable calls 会统计这类调用。
  • 括号中的类型是认领该根节点的入口点规则。 orphan-root 表示仓库中没有任何内容调用它:要么是死代码,要么是配置缺少了对应的入口点规则。
  • 每个栈帧都带有 file:line,因此下一步就是打开该文件。
  • 每个栈帧都附有类型检查器提供的注释——签名,以及压缩为一行的 JSDoc 文本——这让调用栈易于阅读,而不只是需要逐个查找的位置列表。如果栈帧显示为 (…): ReturnType,说明签名超过 80 个字符,通常是一个注入了十几个服务的构造函数。摘要超过 120 个字符时,只会显示开头的句子,且不作标记;只有在无法按句子边界截断、只包含一个句子时,才会截断并标记 …。

缩短只会影响打印出来的树。 JSON 报告会完整保留所有注释,因此需要完整文本的脚本应读取 JSON,而不是解析树。

mermaid 渲染会把所有调用栈绘制为一张流程图,而不是分别绘制。一条调用栈只是一条直线,而直线不过是多了额外步骤的列表;将调用栈一起绘制时,共享的尾部会汇聚在一起——每条命令都到达相同的仓库,每个解析器都以相同的服务结束——这种汇聚正是图示能展现、缩进树无法展现的内容。入口点使用体育场形状,其他节点使用方框;形状而非颜色用于区分,因为读者可能使用任何主题查看图表。图表最多绘制 300 个可调用项;达到上限时会舍弃整个调用栈,而不是截断,以确保图中不会出现指向未绘制节点的边,并会说明省略了多少项。

阅读单个项目的 verdict

在使用 @callidescope/nx 的工作区中,分支实际运行的是某个项目的 gate,它打印的内容不是一份精简报告,而只是它权衡的两项发现,不多不少:

## Call stacks over the depth limit (1)
...the stack, as a tree...

## Callables over the breadth limit (1)
- `GatedLeafService.read` — 3 direct callees, limit 2 (…/gated-leaf.ts)

阅读时请记住以下四点:

  • 这些发现属于被评判项目本身。 gate 会追踪该项目及其 Nx 依赖,然后只评判它所命名的项目自身拥有的内容。依赖项的违规由该依赖项自己的 gate 处理——因此,某个 gate 失败,说明的是一个项目;修复的应是该项目的代码或该项目自己的 callidescope.config.ts。
  • gate 的退出代码就是 verdict,仅此而已。 它不会读取目标位置,因此失败的 gate 没有更改任何已提交文件,而通过的 gate 也没有发布任何内容。
  • 摘要中的 Deepest stack 不是 verdict 使用的数字。 作用域运行的计数描述的是整个追踪范围,包括依赖闭包,因此它报告的最深调用栈通常会超过被评判项目自身拥有的任何调用栈。请采用 verdict 给出的数字,不要采用摘要行打印的数字。对 🔭 Finished an analysis 日志行也要保持同样的谨慎——请参阅 callidescope-triage 技能。
  • ## Read nothing of its own (0 files) 表示本次运行失败,不代表代码有问题。 gate 评判的项目,其自身源码从未被打开,因此绿色 verdict 只意味着它根本没看过。trace 目标会打印相同的内容并通过;只有 gate 会因此失败。

同级的 trace 目标会打印完整报告,包括摘要、Projects 索引(每个项目各占一行,并对照该项目自身的限制),以及 Depth headroom 计分板。要了解项目,请使用 trace;要了解流水线为何标红,请查看 gate。

阅读已提交的项目区块

每个被追踪项目自己的 README 都有一个 ## 🔭 Callidescope 部分,其中的 ### Limits 表格会说明该项目受哪些数字限制:

| Limit | Value |
| --- | --- |
| `maximumDepth` | 4 |
| `maximumBreadth` | none |

没有来源列,也没有第二个来源需要记录。 每个被追踪项目自己的 callidescope.config.ts 都是完整的——它会展开工作区的 projectDefaults,并按需覆盖设置——因此,这两个数字都写在这份 README 旁边的文件中;否则,运行会在开始追踪前就被拒绝。none 表示该项目自己的文件没有指定宽度限制——关于这为何是有意的表述而非遗漏,请参阅 callidescope-configure 技能。

请将上方摘要的 Deepest stack 行与此处的深度限制对照:两者合在一起,就是该项目的 gate 所作决定的全部依据。

定位单个可调用项

depth 和 breadth 接受 <file>#<qualified-name>——文件路径与 callidescope 在每个调用栈帧中打印的限定名称,中间以 # 连接。这与 Python 回溯或 ESLint 规则 ID 使用的格式相同,因此可以直接从报告中复制。

如果一个文件中有多个声明使用相同的限定名称——例如,两个重载,或绑定到同一属性的两个回调——则在末尾加上 :<line> 来区分:

npx callidescope depth --config configuration/callidescope.config.ts --addresses src/foo.service.ts#FooService.bar:118

如果无法判断你指的是哪一个,运行会说明这一点,并打印每个候选项所在的行号,因此只需复制即可得到带行号的地址。

这两个命令都不会写入任何内容、比较目标位置,也不接受 --check、--write、--json、--markdown、--mermaid、--maximum-depth 或 --maximum-breadth。查询只会打印结果。两者都接受所有用于确定图结构的标志——--directories、入口点覆盖项以及两个排除列表——以及必需的 --config 和 --format,因为解析地址仍然需要先追踪工作区。

breadth:它调用什么,以及什么调用它

并排打印可调用对象的直接被调用方和直接调用方——重构或重命名前必须一起弄清楚的两个问题,只有回答清楚后,操作才安全。

  • 重命名。 调用方是需要更新内容的完整清单。在依赖注入代码库中,文本搜索很难回答这个问题:通过 this.someService.load() 发起的调用能被类型检查器找到,却会被搜索类名的 grep 漏掉。
  • 提取。 从一个范围较大的可调用对象中提取职责,意味着移动它所调用对象的 某个子集。被调用方列表是选择切分点的原始材料——找出其中具有共同关注点的子集,并将它们提取出来。
  • 内联。 只有一个调用方且被调用方不多的可调用对象,可以考虑折叠到该调用方中。breadth 会确认“只有一个调用方”这一点,而不是想当然。

即使没有人指出某个可调用对象存在问题,也可以对它运行此工具。运行它不需要先有发现结果。

depth:上方和下方的每一条调用链

打印可调用对象上方和下方的每一条路径——向上到达根节点的每条调用方链,以及向下到达叶节点的每条被调用方链,而不是像 callidescope 自身的报告那样,将每个方向都折叠成最深的一条链。

这一区别正是重点。工作区报告回答“这里有多深”,因此每个入口点只需展示一条路径。depth 回答“这个可调用对象实际属于什么调用关系”,而一个可调用对象从十几个位置被调用,或能到达十几个叶节点,正是它要完整展示的情况。

每个方向最多显示 200 条路径,达到上限时会明确提示。设置上限是因为,被广泛调用且其被调用方也同样广泛分散的工具,会让这些分支相乘——列出每一条路径并不像只显示最深路径那样,天然受到数量限制。

  • 将深度发现转化为计划。 工作区报告会指出调用栈;对其中间的某个栈帧运行 depth,可以显示该栈帧参与的每条 其他 调用链,从而判断折叠某个转发层是否安全,还是有其他三个调用方依赖它。
  • 验证“这个位置似乎不对”的猜测。 调用方树会完整显示该可调用对象实际从哪些位置被使用,而不是将其折叠成一条最深路径。

该选哪个

问题命令
这个工作区中是否有内容过深或过宽?callidescope
如果重命名这个对象,会有什么受影响?breadth
应该从哪里将这个可调用对象一分为二?breadth
可以内联这个对象吗?breadth
可以折叠这一层吗?depth
这个可调用对象实际属于什么调用关系?depth
这个对象应该放在这个文件中吗?depth

范围运行衡量的内容

--directories 会将运行范围缩小到它指定的项目目录,而跟踪仍会跟随从这些目录发出的调用:范围运行会为每个指定项目构建程序,并为这些项目的导入在传递过程中触及的每个项目构建程序——也就是它们的依赖闭包。因此,调用另一个工作区包时会落在真实的栈帧上,而且运行会报告从未指定过的包的栈帧、调用栈和各项目行。这就是闭包,不是泄漏。

如何理解发生变化的深度。 项目自己的 tsconfig.json 从不列出它导入的包,因此,过去范围运行会让指定目录以外的每个调用都落在没有任何已跟踪项目拥有的代码中——而对无所属代码的调用会被视为外部调用,即叶节点,和对已安装包的调用完全一样。调用栈会在包边界处结束,并输出一个普通数字,报告中没有任何说明表明跟踪提前停止了。如果数字上升,却没有对应的代码修改来解释,那就是之前测量不足,如今得到了修正。

由此得出三点,检查数字变化时应对照这几点:

  • 范围深度是一种测量值,不是下限。 ≥ 仍然只表示存在无法跟踪的调用。范围运行从未输出过这种标记,这正是范围内的测量不足一直不可见的原因。
  • 数字不会因运行方式不同而变化。 文件归包含它的最深项目根目录所有,无论哪个程序将它纳入其中。因此,只要每次运行都构建了声明该可调用项的项目,两个范围运行和整个工作区运行对同一可调用项就会得出相同结果;而被拒绝的目标目录所声明的项目不会被构建。
  • 整个工作区运行的闭包不会改变任何内容。 它没有指定任何目录,因此每个项目本来就是起始项目,其闭包就是整个工作区。那里的数字若有变化,原因必然在其他地方。

闭包无法触及的内容,以及排除它们的原因:

  • 有意排除依赖方。 遍历只向下进行。导入范围内项目的项目不会被构建,其调用栈也不会出现——这样就能避免依赖方的修改改变范围内项目的数字。
  • 不包含 package.json 的项目根目录。 清单决定一个目录能否被其他项目依赖;只包含 tsconfig.json 的根目录则只是共享设置,由每个项目读取,而不会被任何项目依赖。如果没有这条规则,这类目录就会把整个工作区都带进来。
  • 工作区根目录。 根目录包含了其他所有项目,因此不可能成为其中任何项目有意义的依赖项。

这两种拒绝规则都只适用于_目标目录_,所以直接指定这两类目录,或运行时不指定任何目录,仍会完整跟踪它们。代价是,调用进入被拒绝的目录时不会解析到任何栈帧。

即使范围只向下延伸,深度和广度仍然都能完整计算;这一点值得说明,因为并非显而易见:两者都沿着可调用项的传递被调用项进行汇总,而这正是向下延伸的方向——也正是闭包能够完整覆盖的内容。两者都不需要依赖方。

发布范围不会随测量范围扩大。 每个项目自己的 write.markdown(以及声明了 write.mermaid 时的该文件)只会为运行指定的项目写入,因此范围运行绝不会重写它仅测量过的依赖项中的内容。

一个实际示例展示了某次真实范围运行会触及哪些项目及其原因,位于 @callidescope/examples 包中的 examples/dependency-closure。

图中包含和不包含的内容

这三条命令都遵循相同的解析规则,因此它们能告诉你的内容也由这些规则决定。

书写形式解析结果
helper()被调用位置的符号,经过导入别名解包
this.service.load()成员名称处的符号——注入依赖的情况
provider.ingest()结构上满足接口的所有类,数量有上限
super.run()检查器解析到的基类声明
new Thing()构造函数(如果它有函数体)
list.map(callback)回调函数作为独立帧;map 本身是外部的
target[key]()无。记录为不可跟踪,而不是猜测

有四点值得记住:

  • 结构匹配不可或缺,因为类经常满足某个接口,却不写 implements。这也意味着,调用方列表可能包含一个运行时实际上从未调用该可调用对象的类——该上限是 @callidescope/graph 中的固定常量,而非可配置的限制,因此看起来宽得不合理的结果意味着需要检查扇出,而不是调高上限。
  • 计算成员调用无法解析。 通过这种方式调用该可调用对象的调用方不会出现在结果中,因此,对于通过计算名称进行分派的代码库,breadth 无法完整覆盖重命名影响。
  • 对_已安装_依赖项的调用是叶节点。 Array.prototype.map 的实现有多深,并不能说明_你的_分层是否过深;统计它还会导致每次无关升级都改变所有数字。同一工作区中另一个项目的调用不属于此类——无论运行范围如何设定,它都会解析为一个真实帧。
  • excludeCallees glob 会从图中彻底移除,因此,配置排除的可调用对象(通常是日志记录器)不会出现在任何列表中,也不会计入任何对象的深度或广度。

测量深度前会先折叠循环,因此,三个相互递归的帧会作为一次整体计入三个帧——这是一个没有上限的调用栈的真实下限。这就是为什么多次运行的数字不会变化:如果在遍历中途检测到重复访问,答案就会取决于哪条路径先到达,而一个数字会自行变化的代码检查工具无法用作门禁。

深度只有相对于根才有意义,而框架代码库中的大多数代码都是由框架调用,而不是由仓库调用。因此,根可以配置为装饰方法、生命周期钩子、引导程序、索引导出;任何没有调用方的内容都会提升为孤立根,而不会被丢弃。

阅读之后

若要缩小范围、设置门禁或发布一次运行结果,或更改上述任何阈值,请使用 callidescope-configure skill。如果运行失败或报告看起来过时,请使用 callidescope-triage。

重构会改变调用图,因此所有已提交的报告都会过时。更改落地后,使用写入配置重新运行一次,并在重新生成之前运行 lint:每个帧都带有 file:line,因此,若格式化工具对类成员排序,其后的所有内容行号都会变化;在排序前生成的报告一经落地就会过时。

评论

支持 Markdown