@vibe-agent-toolkit/agent-skills 0.1.42 → 0.2.0-rc.10

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 (322) hide show
  1. package/README.md +5 -5
  2. package/dist/builder.d.ts.map +1 -1
  3. package/dist/builder.js +7 -17
  4. package/dist/builder.js.map +1 -1
  5. package/dist/content-type-routing.d.ts +32 -0
  6. package/dist/content-type-routing.d.ts.map +1 -1
  7. package/dist/content-type-routing.js +23 -0
  8. package/dist/content-type-routing.js.map +1 -1
  9. package/dist/files-config.d.ts +34 -1
  10. package/dist/files-config.d.ts.map +1 -1
  11. package/dist/files-config.js +277 -39
  12. package/dist/files-config.js.map +1 -1
  13. package/dist/fs-attribution.d.ts +45 -0
  14. package/dist/fs-attribution.d.ts.map +1 -0
  15. package/dist/fs-attribution.js +57 -0
  16. package/dist/fs-attribution.js.map +1 -0
  17. package/dist/import.d.ts.map +1 -1
  18. package/dist/import.js +0 -4
  19. package/dist/import.js.map +1 -1
  20. package/dist/index.d.ts +13 -8
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +25 -7
  23. package/dist/index.js.map +1 -1
  24. package/dist/inventory/detectors/declared-but-missing.d.ts +1 -1
  25. package/dist/inventory/detectors/declared-but-missing.d.ts.map +1 -1
  26. package/dist/inventory/detectors/marketplace-source-missing.d.ts +1 -1
  27. package/dist/inventory/detectors/marketplace-source-missing.d.ts.map +1 -1
  28. package/dist/inventory/detectors/present-but-undeclared.d.ts +1 -1
  29. package/dist/inventory/detectors/present-but-undeclared.d.ts.map +1 -1
  30. package/dist/inventory/detectors/reference-target-missing.d.ts +1 -1
  31. package/dist/inventory/detectors/reference-target-missing.d.ts.map +1 -1
  32. package/dist/inventory/index.d.ts +1 -1
  33. package/dist/inventory/index.d.ts.map +1 -1
  34. package/dist/inventory/index.js +1 -1
  35. package/dist/inventory/index.js.map +1 -1
  36. package/dist/inventory/serialize.d.ts +4 -4
  37. package/dist/inventory/serialize.d.ts.map +1 -1
  38. package/dist/inventory/serialize.js +6 -6
  39. package/dist/inventory/serialize.js.map +1 -1
  40. package/dist/inventory/types.d.ts +8 -0
  41. package/dist/inventory/types.d.ts.map +1 -1
  42. package/dist/inventory/types.js.map +1 -1
  43. package/dist/link-graph-members.d.ts +18 -0
  44. package/dist/link-graph-members.d.ts.map +1 -0
  45. package/dist/link-graph-members.js +18 -0
  46. package/dist/link-graph-members.js.map +1 -0
  47. package/dist/parsers/frontmatter-parser.d.ts.map +1 -1
  48. package/dist/parsers/frontmatter-parser.js +30 -6
  49. package/dist/parsers/frontmatter-parser.js.map +1 -1
  50. package/dist/plugin-distribution-layout.d.ts +70 -6
  51. package/dist/plugin-distribution-layout.d.ts.map +1 -1
  52. package/dist/plugin-distribution-layout.js +84 -11
  53. package/dist/plugin-distribution-layout.js.map +1 -1
  54. package/dist/post-build-checks.d.ts +38 -1
  55. package/dist/post-build-checks.d.ts.map +1 -1
  56. package/dist/post-build-checks.js +68 -17
  57. package/dist/post-build-checks.js.map +1 -1
  58. package/dist/projection/skill-extent.d.ts +247 -0
  59. package/dist/projection/skill-extent.d.ts.map +1 -0
  60. package/dist/projection/skill-extent.js +560 -0
  61. package/dist/projection/skill-extent.js.map +1 -0
  62. package/dist/schemas/agent-skill-frontmatter.js +1 -1
  63. package/dist/schemas/agent-skill-frontmatter.js.map +1 -1
  64. package/dist/schemas/allowed-tools.d.ts +31 -0
  65. package/dist/schemas/allowed-tools.d.ts.map +1 -0
  66. package/dist/schemas/allowed-tools.js +88 -0
  67. package/dist/schemas/allowed-tools.js.map +1 -0
  68. package/dist/schemas/index.d.ts +1 -0
  69. package/dist/schemas/index.d.ts.map +1 -1
  70. package/dist/schemas/index.js +1 -0
  71. package/dist/schemas/index.js.map +1 -1
  72. package/dist/schemas/installed-plugins-registry.d.ts.map +1 -1
  73. package/dist/schemas/installed-plugins-registry.js +7 -1
  74. package/dist/schemas/installed-plugins-registry.js.map +1 -1
  75. package/dist/schemas/marketplace-manifest.d.ts.map +1 -1
  76. package/dist/schemas/marketplace-manifest.js +17 -6
  77. package/dist/schemas/marketplace-manifest.js.map +1 -1
  78. package/dist/skill-identity.d.ts +18 -3
  79. package/dist/skill-identity.d.ts.map +1 -1
  80. package/dist/skill-identity.js +25 -6
  81. package/dist/skill-identity.js.map +1 -1
  82. package/dist/skill-packager.d.ts +144 -20
  83. package/dist/skill-packager.d.ts.map +1 -1
  84. package/dist/skill-packager.js +447 -105
  85. package/dist/skill-packager.js.map +1 -1
  86. package/dist/skill-source/content-hash.d.ts.map +1 -1
  87. package/dist/skill-source/content-hash.js +21 -10
  88. package/dist/skill-source/content-hash.js.map +1 -1
  89. package/dist/skill-source/fetch-cache.d.ts.map +1 -1
  90. package/dist/skill-source/fetch-cache.js +0 -5
  91. package/dist/skill-source/fetch-cache.js.map +1 -1
  92. package/dist/skill-source/git-clone.d.ts +1 -1
  93. package/dist/skill-source/git-clone.d.ts.map +1 -1
  94. package/dist/skill-source/git-clone.js +10 -10
  95. package/dist/skill-source/git-clone.js.map +1 -1
  96. package/dist/skill-source/sources/url-source.d.ts.map +1 -1
  97. package/dist/skill-source/sources/url-source.js +2 -2
  98. package/dist/skill-source/sources/url-source.js.map +1 -1
  99. package/dist/skill-source/stage.d.ts.map +1 -1
  100. package/dist/skill-source/stage.js +0 -6
  101. package/dist/skill-source/stage.js.map +1 -1
  102. package/dist/skill-test/baseline-delta.d.ts +1575 -0
  103. package/dist/skill-test/baseline-delta.d.ts.map +1 -0
  104. package/dist/skill-test/baseline-delta.js +272 -0
  105. package/dist/skill-test/baseline-delta.js.map +1 -0
  106. package/dist/skill-test/baseline-integrity.d.ts +944 -0
  107. package/dist/skill-test/baseline-integrity.d.ts.map +1 -0
  108. package/dist/skill-test/baseline-integrity.js +3090 -0
  109. package/dist/skill-test/baseline-integrity.js.map +1 -0
  110. package/dist/skill-test/build-hook.d.ts +3 -2
  111. package/dist/skill-test/build-hook.d.ts.map +1 -1
  112. package/dist/skill-test/build-hook.js +4 -5
  113. package/dist/skill-test/build-hook.js.map +1 -1
  114. package/dist/skill-test/configure-writer.js +1 -1
  115. package/dist/skill-test/configure-writer.js.map +1 -1
  116. package/dist/skill-test/declared-env.d.ts +5 -4
  117. package/dist/skill-test/declared-env.d.ts.map +1 -1
  118. package/dist/skill-test/declared-env.js +8 -9
  119. package/dist/skill-test/declared-env.js.map +1 -1
  120. package/dist/skill-test/eval-executor.d.ts +21 -5
  121. package/dist/skill-test/eval-executor.d.ts.map +1 -1
  122. package/dist/skill-test/eval-executor.js +6 -6
  123. package/dist/skill-test/eval-executor.js.map +1 -1
  124. package/dist/skill-test/eval-fragment.d.ts +106 -28
  125. package/dist/skill-test/eval-fragment.d.ts.map +1 -1
  126. package/dist/skill-test/eval-fragment.js +78 -16
  127. package/dist/skill-test/eval-fragment.js.map +1 -1
  128. package/dist/skill-test/eval-grader.d.ts +65 -7
  129. package/dist/skill-test/eval-grader.d.ts.map +1 -1
  130. package/dist/skill-test/eval-grader.js +263 -64
  131. package/dist/skill-test/eval-grader.js.map +1 -1
  132. package/dist/skill-test/eval-inputs.d.ts +110 -8
  133. package/dist/skill-test/eval-inputs.d.ts.map +1 -1
  134. package/dist/skill-test/eval-inputs.js +177 -22
  135. package/dist/skill-test/eval-inputs.js.map +1 -1
  136. package/dist/skill-test/eval-lint.d.ts +17 -4
  137. package/dist/skill-test/eval-lint.d.ts.map +1 -1
  138. package/dist/skill-test/eval-lint.js +22 -9
  139. package/dist/skill-test/eval-lint.js.map +1 -1
  140. package/dist/skill-test/eval-suite-isolation.d.ts.map +1 -1
  141. package/dist/skill-test/eval-suite-isolation.js +8 -5
  142. package/dist/skill-test/eval-suite-isolation.js.map +1 -1
  143. package/dist/skill-test/evals-template.d.ts +1 -1
  144. package/dist/skill-test/evals-template.d.ts.map +1 -1
  145. package/dist/skill-test/evals-template.js +1 -3
  146. package/dist/skill-test/evals-template.js.map +1 -1
  147. package/dist/skill-test/executor-prompt.d.ts +15 -7
  148. package/dist/skill-test/executor-prompt.d.ts.map +1 -1
  149. package/dist/skill-test/executor-prompt.js +10 -7
  150. package/dist/skill-test/executor-prompt.js.map +1 -1
  151. package/dist/skill-test/failure-reason.d.ts +113 -0
  152. package/dist/skill-test/failure-reason.d.ts.map +1 -0
  153. package/dist/skill-test/failure-reason.js +124 -0
  154. package/dist/skill-test/failure-reason.js.map +1 -0
  155. package/dist/skill-test/fragment-merge.d.ts +3 -20
  156. package/dist/skill-test/fragment-merge.d.ts.map +1 -1
  157. package/dist/skill-test/fragment-merge.js +67 -8
  158. package/dist/skill-test/fragment-merge.js.map +1 -1
  159. package/dist/skill-test/grader-prompt.d.ts +29 -9
  160. package/dist/skill-test/grader-prompt.d.ts.map +1 -1
  161. package/dist/skill-test/grader-prompt.js +264 -68
  162. package/dist/skill-test/grader-prompt.js.map +1 -1
  163. package/dist/skill-test/grader-text.d.ts +145 -0
  164. package/dist/skill-test/grader-text.d.ts.map +1 -0
  165. package/dist/skill-test/grader-text.js +410 -0
  166. package/dist/skill-test/grader-text.js.map +1 -0
  167. package/dist/skill-test/grading-adapter.d.ts +38 -2
  168. package/dist/skill-test/grading-adapter.d.ts.map +1 -1
  169. package/dist/skill-test/grading-adapter.js +57 -9
  170. package/dist/skill-test/grading-adapter.js.map +1 -1
  171. package/dist/skill-test/grading-schema.d.ts +160 -5
  172. package/dist/skill-test/grading-schema.d.ts.map +1 -1
  173. package/dist/skill-test/grading-schema.js +66 -7
  174. package/dist/skill-test/grading-schema.js.map +1 -1
  175. package/dist/skill-test/harness-location.d.ts +3 -2
  176. package/dist/skill-test/harness-location.d.ts.map +1 -1
  177. package/dist/skill-test/harness-location.js +5 -16
  178. package/dist/skill-test/harness-location.js.map +1 -1
  179. package/dist/skill-test/index.d.ts +2 -2
  180. package/dist/skill-test/index.d.ts.map +1 -1
  181. package/dist/skill-test/index.js +2 -2
  182. package/dist/skill-test/index.js.map +1 -1
  183. package/dist/skill-test/lock.d.ts +58 -1
  184. package/dist/skill-test/lock.d.ts.map +1 -1
  185. package/dist/skill-test/lock.js +78 -6
  186. package/dist/skill-test/lock.js.map +1 -1
  187. package/dist/skill-test/pipeline.d.ts +19 -1
  188. package/dist/skill-test/pipeline.d.ts.map +1 -1
  189. package/dist/skill-test/pipeline.js +21 -10
  190. package/dist/skill-test/pipeline.js.map +1 -1
  191. package/dist/skill-test/preflight.d.ts +1 -1
  192. package/dist/skill-test/preflight.d.ts.map +1 -1
  193. package/dist/skill-test/preflight.js +59 -16
  194. package/dist/skill-test/preflight.js.map +1 -1
  195. package/dist/skill-test/prompt-invariants.d.ts +4 -2
  196. package/dist/skill-test/prompt-invariants.d.ts.map +1 -1
  197. package/dist/skill-test/prompt-invariants.js +5 -4
  198. package/dist/skill-test/prompt-invariants.js.map +1 -1
  199. package/dist/skill-test/run-harness.d.ts +665 -85
  200. package/dist/skill-test/run-harness.d.ts.map +1 -1
  201. package/dist/skill-test/run-harness.js +1522 -237
  202. package/dist/skill-test/run-harness.js.map +1 -1
  203. package/dist/skill-test/staging.d.ts +20 -8
  204. package/dist/skill-test/staging.d.ts.map +1 -1
  205. package/dist/skill-test/staging.js +25 -18
  206. package/dist/skill-test/staging.js.map +1 -1
  207. package/dist/skill-test/tier-plan.d.ts +1 -1
  208. package/dist/skill-test/tier-plan.js +1 -1
  209. package/dist/skill-test/tool-eval-schema.d.ts +6 -6
  210. package/dist/skill-test/tool-eval-schema.js +2 -2
  211. package/dist/skill-test/vendor-manifest.d.ts +2 -1
  212. package/dist/skill-test/vendor-manifest.d.ts.map +1 -1
  213. package/dist/skill-test/vendor-manifest.js +10 -10
  214. package/dist/skill-test/vendor-manifest.js.map +1 -1
  215. package/dist/test-input.d.ts +59 -8
  216. package/dist/test-input.d.ts.map +1 -1
  217. package/dist/test-input.js +64 -19
  218. package/dist/test-input.js.map +1 -1
  219. package/dist/validators/agent-instruction-presence.d.ts +1 -1
  220. package/dist/validators/agent-instruction-presence.d.ts.map +1 -1
  221. package/dist/validators/agent-instruction-presence.js +45 -2
  222. package/dist/validators/agent-instruction-presence.js.map +1 -1
  223. package/dist/validators/bundled-resource-link-detection.d.ts +6 -4
  224. package/dist/validators/bundled-resource-link-detection.d.ts.map +1 -1
  225. package/dist/validators/bundled-resource-link-detection.js +39 -39
  226. package/dist/validators/bundled-resource-link-detection.js.map +1 -1
  227. package/dist/validators/compat-detectors.d.ts +18 -1
  228. package/dist/validators/compat-detectors.d.ts.map +1 -1
  229. package/dist/validators/compat-detectors.js +47 -20
  230. package/dist/validators/compat-detectors.js.map +1 -1
  231. package/dist/validators/cross-skill-dependency-detection.d.ts +1 -1
  232. package/dist/validators/cross-skill-dependency-detection.d.ts.map +1 -1
  233. package/dist/validators/cross-skill-dependency-detection.js +1 -1
  234. package/dist/validators/cross-skill-dependency-detection.js.map +1 -1
  235. package/dist/validators/description-style-detection.d.ts +1 -1
  236. package/dist/validators/description-style-detection.d.ts.map +1 -1
  237. package/dist/validators/description-style-detection.js +1 -1
  238. package/dist/validators/description-style-detection.js.map +1 -1
  239. package/dist/validators/format-detection.d.ts +3 -0
  240. package/dist/validators/format-detection.d.ts.map +1 -1
  241. package/dist/validators/format-detection.js +41 -26
  242. package/dist/validators/format-detection.js.map +1 -1
  243. package/dist/validators/frontmatter-validation.d.ts +1 -1
  244. package/dist/validators/frontmatter-validation.d.ts.map +1 -1
  245. package/dist/validators/frontmatter-validation.js +2 -2
  246. package/dist/validators/frontmatter-validation.js.map +1 -1
  247. package/dist/validators/imperative-body-detection.d.ts +1 -1
  248. package/dist/validators/imperative-body-detection.d.ts.map +1 -1
  249. package/dist/validators/imperative-body-detection.js +1 -1
  250. package/dist/validators/imperative-body-detection.js.map +1 -1
  251. package/dist/validators/index.d.ts +1 -1
  252. package/dist/validators/index.d.ts.map +1 -1
  253. package/dist/validators/kebab-case-detection.d.ts +1 -1
  254. package/dist/validators/kebab-case-detection.d.ts.map +1 -1
  255. package/dist/validators/kebab-case-detection.js +1 -1
  256. package/dist/validators/kebab-case-detection.js.map +1 -1
  257. package/dist/validators/marketplace-validator.d.ts.map +1 -1
  258. package/dist/validators/marketplace-validator.js +7 -2
  259. package/dist/validators/marketplace-validator.js.map +1 -1
  260. package/dist/validators/mcp-tool-qualification.d.ts +158 -0
  261. package/dist/validators/mcp-tool-qualification.d.ts.map +1 -0
  262. package/dist/validators/mcp-tool-qualification.js +467 -0
  263. package/dist/validators/mcp-tool-qualification.js.map +1 -0
  264. package/dist/validators/packaged-size-limit.d.ts +296 -0
  265. package/dist/validators/packaged-size-limit.d.ts.map +1 -0
  266. package/dist/validators/packaged-size-limit.js +492 -0
  267. package/dist/validators/packaged-size-limit.js.map +1 -0
  268. package/dist/validators/packaging-validator.d.ts +149 -10
  269. package/dist/validators/packaging-validator.d.ts.map +1 -1
  270. package/dist/validators/packaging-validator.js +371 -55
  271. package/dist/validators/packaging-validator.js.map +1 -1
  272. package/dist/validators/plugin-hosted-shape.d.ts +4 -2
  273. package/dist/validators/plugin-hosted-shape.d.ts.map +1 -1
  274. package/dist/validators/plugin-hosted-shape.js +6 -7
  275. package/dist/validators/plugin-hosted-shape.js.map +1 -1
  276. package/dist/validators/plugin-recommended-fields.d.ts +1 -1
  277. package/dist/validators/plugin-recommended-fields.d.ts.map +1 -1
  278. package/dist/validators/plugin-recommended-fields.js +1 -1
  279. package/dist/validators/plugin-recommended-fields.js.map +1 -1
  280. package/dist/validators/referenced-path-missing.d.ts +211 -0
  281. package/dist/validators/referenced-path-missing.d.ts.map +1 -0
  282. package/dist/validators/referenced-path-missing.js +378 -0
  283. package/dist/validators/referenced-path-missing.js.map +1 -0
  284. package/dist/validators/registry-validator.d.ts.map +1 -1
  285. package/dist/validators/registry-validator.js +1 -2
  286. package/dist/validators/registry-validator.js.map +1 -1
  287. package/dist/validators/rule-engine/index.d.ts +1 -1
  288. package/dist/validators/rule-engine/rule-context.d.ts +20 -3
  289. package/dist/validators/rule-engine/rule-context.d.ts.map +1 -1
  290. package/dist/validators/rule-engine/rule-context.js +3 -1
  291. package/dist/validators/rule-engine/rule-context.js.map +1 -1
  292. package/dist/validators/rule-engine/rule-engine.d.ts +2 -2
  293. package/dist/validators/rule-engine/rule-engine.d.ts.map +1 -1
  294. package/dist/validators/rule-engine/rule-engine.js +52 -13
  295. package/dist/validators/rule-engine/rule-engine.js.map +1 -1
  296. package/dist/validators/skill-validator.d.ts.map +1 -1
  297. package/dist/validators/skill-validator.js +91 -54
  298. package/dist/validators/skill-validator.js.map +1 -1
  299. package/dist/validators/source-only-codes.js +1 -1
  300. package/dist/validators/source-only-codes.js.map +1 -1
  301. package/dist/validators/types.d.ts +21 -1
  302. package/dist/validators/types.d.ts.map +1 -1
  303. package/dist/validators/unified-validator.d.ts.map +1 -1
  304. package/dist/validators/unified-validator.js +4 -5
  305. package/dist/validators/unified-validator.js.map +1 -1
  306. package/dist/validators/validation-rules.d.ts +4 -4
  307. package/dist/validators/validation-rules.d.ts.map +1 -1
  308. package/dist/validators/validation-rules.js +9 -38
  309. package/dist/validators/validation-rules.js.map +1 -1
  310. package/dist/validators/walker-to-issues.d.ts +9 -3
  311. package/dist/validators/walker-to-issues.d.ts.map +1 -1
  312. package/dist/validators/walker-to-issues.js +64 -19
  313. package/dist/validators/walker-to-issues.js.map +1 -1
  314. package/dist/walk-link-graph.d.ts +64 -5
  315. package/dist/walk-link-graph.d.ts.map +1 -1
  316. package/dist/walk-link-graph.js +376 -92
  317. package/dist/walk-link-graph.js.map +1 -1
  318. package/package.json +14 -22
  319. package/dist/skill-test/exit-codes.d.ts +0 -97
  320. package/dist/skill-test/exit-codes.d.ts.map +0 -1
  321. package/dist/skill-test/exit-codes.js +0 -141
  322. package/dist/skill-test/exit-codes.js.map +0 -1
@@ -0,0 +1,158 @@
1
+ /**
2
+ * MCP_TOOL_NAME_UNQUALIFIED — a skill document tells an agent to call an MCP
3
+ * tool by its bare name, in a document that elsewhere spells that same tool
4
+ * fully-qualified.
5
+ *
6
+ * Anthropic's skill-authoring guidance: "Without the server prefix, Claude may
7
+ * fail to locate the tool, especially when multiple MCP servers are available."
8
+ * VAT's own `vat skill review` checklist has carried this as a `[A]` manual item
9
+ * marked "Not enforced by any validation code; a shift-left candidate awaiting
10
+ * corpus evidence per docs/validation-rule-design.md, since a bare identifier in
11
+ * prose is only a defect when the skill actually drives MCP." The reservation is
12
+ * exactly right, and it is what shapes the detector below.
13
+ *
14
+ * ## The document supplies its own vocabulary
15
+ *
16
+ * A bare snake_case identifier is not evidence of anything — `page_size`,
17
+ * `next_page_token` and `whiteboard_id` are all over these documents. What makes
18
+ * an identifier an MCP *tool name* is that the SAME document also writes it
19
+ * fully-qualified, in one of the two spellings a reader will meet:
20
+ *
21
+ * - `mcp__<server>__<tool>` — the Claude Code form, also how `allowed-tools`
22
+ * frontmatter names them.
23
+ * - `ServerName:tool_name` — the API form Anthropic's guidance prescribes.
24
+ *
25
+ * So the detector never guesses. It reads the qualified names the document
26
+ * already contains, and then reports the places that document names one of those
27
+ * same tools bare. A document that does not drive MCP has an empty vocabulary and
28
+ * cannot produce a finding — which dissolves the checklist's reservation rather
29
+ * than arguing with it.
30
+ *
31
+ * ## Prose supplies the vocabulary; frontmatter does not
32
+ *
33
+ * Leading YAML frontmatter is stripped here, in the detector, before anything is
34
+ * read from it. An `allowed-tools:` list of `mcp__…` names is a manifest, not the
35
+ * document contradicting itself, so it must not seed the vocabulary — and a bare
36
+ * name in the body below it is then simply a document that never qualified the
37
+ * tool at all.
38
+ *
39
+ * Enforcing that HERE rather than at each call site is deliberate, and it is the
40
+ * only place it has ever held. ⛔ An earlier version of this paragraph said the
41
+ * SKILL.md lane in `packaging-validator.ts` "happened to pass a
42
+ * frontmatter-stripped slice" while the bundled-`.md` lane passed whole files.
43
+ * It does not and never did: `parseFileCached(...).content` is the raw source
44
+ * verbatim — that file says so itself, 190 lines above the call — so BOTH lanes
45
+ * fed frontmatter to the detector until this stripping landed.
46
+ *
47
+ * ## The uppercase rule in the API form, and why it is load-bearing
48
+ *
49
+ * The server half must be spelled the way Anthropic's own examples spell one —
50
+ * uppercase FIRST, no underscore (`GitHub:create_issue`,
51
+ * `BigQuery:bigquery_schema`). Without that rule the
52
+ * pattern also matches two things that are emphatically not MCP tools:
53
+ *
54
+ * - **Node builtin specifiers** — `node:child_process` yields a "tool" named
55
+ * `child_process`, and VAT's own `packages/utils/README.md` then reports a
56
+ * finding on a table of exported function names.
57
+ * - **OAuth scope strings** — `whiteboard:read:list_whiteboards` and
58
+ * `cloud_recording:read:list_user_recordings`, both live in a partner-built
59
+ * plugin in the install corpus.
60
+ *
61
+ * Neither was visible on the two adopter corpora; both were visible on the
62
+ * authoring project, which is precisely the population check
63
+ * `docs/validation-rule-design.md` requires and the reason it requires it.
64
+ *
65
+ * ## Measured fire rate (re-measured after the camelCase fix below)
66
+ *
67
+ * Population = documents that spell at least one MCP tool fully-qualified.
68
+ *
69
+ * | Corpus | docs | population | firing | occurrences |
70
+ * |---|---|---|---|---|
71
+ * | VAT itself (`packages/**\/*.md`, dist + node_modules excluded) | 206 | 3 | **0** | 0 |
72
+ * | installed skills (`SKILL.md` under `~/.claude/plugins`, `~/.claude/skills`) | 678 | 28 | 7 | 11 |
73
+ *
74
+ * ⛔ **Every number this table used to carry was taken by the camelCase-blind
75
+ * matcher, so re-taking them was not optional.** A corpus measured by a matcher
76
+ * that cannot see a whole family of names has already excluded that family from
77
+ * its own population count, and "zero false positives" over such a corpus is a
78
+ * statement about the blindness as much as about the rule. Both rows above were
79
+ * re-run with the fixed detector, driving the exported functions directly, over
80
+ * a corpus re-enumerated on the day.
81
+ *
82
+ * What moved, A/B on the same files with HEAD's module and this one:
83
+ *
84
+ * - **Authoring project: population 2 → 3.** One document newly enters the
85
+ * population, because it spells two camelCase tools (`generateHaiku`,
86
+ * `generateName`) that the old matcher could not see at all. Firing stays 0.
87
+ * - **Installed skills: nothing moved** — 28 / 7 / 11 before and after,
88
+ * character for character, with no vocabulary entry gained or lost. The
89
+ * installed corpus is snake_case throughout, which is Anthropic's own
90
+ * convention and therefore exactly the population least able to expose this
91
+ * defect. Its silence is not evidence the defect was small.
92
+ * - The corpus itself grew 677 → 678 `SKILL.md` between the two measurement
93
+ * dates; the delta above is A/B on the SAME 678 files, so it is a property of
94
+ * the detector rather than of the corpus.
95
+ *
96
+ * All 11 occurrences were read: every one names a tool bare that the same
97
+ * document qualifies in its own prose. Zero false positives.
98
+ *
99
+ * The SHIPPED command was re-run on the same corpus rather than trusted to
100
+ * agree with the probe — `vat audit --user`, 852 skills scanned, reproducing
101
+ * the installed-skills row occurrence for occurrence and line for line (plus
102
+ * the 4 linked-file occurrences `docs/validation-codes.md` records, which the
103
+ * probe above does not traverse).
104
+ *
105
+ * The frontmatter-including detector, run over the same 678 documents, reports
106
+ * 19 occurrences from the same 7 documents. The extra 8 are four copies of one
107
+ * skill naming `create_repository` and `create_branch` bare, whose only
108
+ * qualified spelling is its `allowed-tools:` frontmatter.
109
+ *
110
+ * ⛔ Those 8 WERE shipped. This paragraph used to claim the SKILL.md lane had
111
+ * "always passed a frontmatter-stripped slice" and that the 19 therefore
112
+ * described the probe rather than the product. `parseFileCached(...).content` is
113
+ * the raw file, frontmatter and all, so the shipped lane emitted every one of
114
+ * them. The 19 → 11 delta is a real behaviour change this detector made, not a
115
+ * correction to a probe.
116
+ *
117
+ * `warning`, not `error`: with a single MCP server mounted the bare name
118
+ * usually resolves, so the skill is degraded rather than broken.
119
+ */
120
+ import { type ValidationIssue } from '@vibe-agent-toolkit/schema';
121
+ /**
122
+ * Every MCP tool name `content` spells fully-qualified in its body, in either
123
+ * spelling. Frontmatter supplies nothing; see the module docstring.
124
+ *
125
+ * Exported for tests: this set IS the detector's premise, and pinning it directly
126
+ * is cheaper and clearer than inferring it from emitted issues.
127
+ */
128
+ export declare function qualifiedMcpToolNames(content: string): Set<string>;
129
+ /**
130
+ * Emit one `MCP_TOOL_NAME_UNQUALIFIED` per (line, bare tool name).
131
+ *
132
+ * Per instance rather than per document, each carrying the bare tool name as
133
+ * `link`, because `applyAllowFilter` matches an allow glob against `location` OR
134
+ * `link`. That is what lets an adopter waive one identifier:
135
+ *
136
+ * ```yaml
137
+ * validation:
138
+ * allow:
139
+ * MCP_TOOL_NAME_UNQUALIFIED:
140
+ * - paths: ["get_me"]
141
+ * reason: "Named bare in the availability-probe step on purpose; the
142
+ * qualified form is three lines above."
143
+ * ```
144
+ *
145
+ * …while a different bare tool name in the same document still fires. Waiver
146
+ * granularity is the emitter's decision, not the allow machinery's.
147
+ *
148
+ * Only inline code spans count. An agent copies what is in a code span; the same
149
+ * word in running prose is discussion of the tool rather than an instruction to
150
+ * call it. This is a design choice, not a measured one — the corpus numbers in
151
+ * the module docstring were all taken with the code-span rule already applied, so
152
+ * nothing here says what admitting prose would have cost.
153
+ *
154
+ * @param content Raw markdown of one skill document, frontmatter and all.
155
+ * @param docLocation Project-relative path of that document, for the anchor.
156
+ */
157
+ export declare function collectUnqualifiedMcpToolIssues(content: string, docLocation: string, issues: ValidationIssue[]): void;
158
+ //# sourceMappingURL=mcp-tool-qualification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-tool-qualification.d.ts","sourceRoot":"","sources":["../../src/validators/mcp-tool-qualification.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsHG;AAEH,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAgRjF;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAElE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,eAAe,EAAE,GACxB,IAAI,CA6CN"}
@@ -0,0 +1,467 @@
1
+ /**
2
+ * MCP_TOOL_NAME_UNQUALIFIED — a skill document tells an agent to call an MCP
3
+ * tool by its bare name, in a document that elsewhere spells that same tool
4
+ * fully-qualified.
5
+ *
6
+ * Anthropic's skill-authoring guidance: "Without the server prefix, Claude may
7
+ * fail to locate the tool, especially when multiple MCP servers are available."
8
+ * VAT's own `vat skill review` checklist has carried this as a `[A]` manual item
9
+ * marked "Not enforced by any validation code; a shift-left candidate awaiting
10
+ * corpus evidence per docs/validation-rule-design.md, since a bare identifier in
11
+ * prose is only a defect when the skill actually drives MCP." The reservation is
12
+ * exactly right, and it is what shapes the detector below.
13
+ *
14
+ * ## The document supplies its own vocabulary
15
+ *
16
+ * A bare snake_case identifier is not evidence of anything — `page_size`,
17
+ * `next_page_token` and `whiteboard_id` are all over these documents. What makes
18
+ * an identifier an MCP *tool name* is that the SAME document also writes it
19
+ * fully-qualified, in one of the two spellings a reader will meet:
20
+ *
21
+ * - `mcp__<server>__<tool>` — the Claude Code form, also how `allowed-tools`
22
+ * frontmatter names them.
23
+ * - `ServerName:tool_name` — the API form Anthropic's guidance prescribes.
24
+ *
25
+ * So the detector never guesses. It reads the qualified names the document
26
+ * already contains, and then reports the places that document names one of those
27
+ * same tools bare. A document that does not drive MCP has an empty vocabulary and
28
+ * cannot produce a finding — which dissolves the checklist's reservation rather
29
+ * than arguing with it.
30
+ *
31
+ * ## Prose supplies the vocabulary; frontmatter does not
32
+ *
33
+ * Leading YAML frontmatter is stripped here, in the detector, before anything is
34
+ * read from it. An `allowed-tools:` list of `mcp__…` names is a manifest, not the
35
+ * document contradicting itself, so it must not seed the vocabulary — and a bare
36
+ * name in the body below it is then simply a document that never qualified the
37
+ * tool at all.
38
+ *
39
+ * Enforcing that HERE rather than at each call site is deliberate, and it is the
40
+ * only place it has ever held. ⛔ An earlier version of this paragraph said the
41
+ * SKILL.md lane in `packaging-validator.ts` "happened to pass a
42
+ * frontmatter-stripped slice" while the bundled-`.md` lane passed whole files.
43
+ * It does not and never did: `parseFileCached(...).content` is the raw source
44
+ * verbatim — that file says so itself, 190 lines above the call — so BOTH lanes
45
+ * fed frontmatter to the detector until this stripping landed.
46
+ *
47
+ * ## The uppercase rule in the API form, and why it is load-bearing
48
+ *
49
+ * The server half must be spelled the way Anthropic's own examples spell one —
50
+ * uppercase FIRST, no underscore (`GitHub:create_issue`,
51
+ * `BigQuery:bigquery_schema`). Without that rule the
52
+ * pattern also matches two things that are emphatically not MCP tools:
53
+ *
54
+ * - **Node builtin specifiers** — `node:child_process` yields a "tool" named
55
+ * `child_process`, and VAT's own `packages/utils/README.md` then reports a
56
+ * finding on a table of exported function names.
57
+ * - **OAuth scope strings** — `whiteboard:read:list_whiteboards` and
58
+ * `cloud_recording:read:list_user_recordings`, both live in a partner-built
59
+ * plugin in the install corpus.
60
+ *
61
+ * Neither was visible on the two adopter corpora; both were visible on the
62
+ * authoring project, which is precisely the population check
63
+ * `docs/validation-rule-design.md` requires and the reason it requires it.
64
+ *
65
+ * ## Measured fire rate (re-measured after the camelCase fix below)
66
+ *
67
+ * Population = documents that spell at least one MCP tool fully-qualified.
68
+ *
69
+ * | Corpus | docs | population | firing | occurrences |
70
+ * |---|---|---|---|---|
71
+ * | VAT itself (`packages/**\/*.md`, dist + node_modules excluded) | 206 | 3 | **0** | 0 |
72
+ * | installed skills (`SKILL.md` under `~/.claude/plugins`, `~/.claude/skills`) | 678 | 28 | 7 | 11 |
73
+ *
74
+ * ⛔ **Every number this table used to carry was taken by the camelCase-blind
75
+ * matcher, so re-taking them was not optional.** A corpus measured by a matcher
76
+ * that cannot see a whole family of names has already excluded that family from
77
+ * its own population count, and "zero false positives" over such a corpus is a
78
+ * statement about the blindness as much as about the rule. Both rows above were
79
+ * re-run with the fixed detector, driving the exported functions directly, over
80
+ * a corpus re-enumerated on the day.
81
+ *
82
+ * What moved, A/B on the same files with HEAD's module and this one:
83
+ *
84
+ * - **Authoring project: population 2 → 3.** One document newly enters the
85
+ * population, because it spells two camelCase tools (`generateHaiku`,
86
+ * `generateName`) that the old matcher could not see at all. Firing stays 0.
87
+ * - **Installed skills: nothing moved** — 28 / 7 / 11 before and after,
88
+ * character for character, with no vocabulary entry gained or lost. The
89
+ * installed corpus is snake_case throughout, which is Anthropic's own
90
+ * convention and therefore exactly the population least able to expose this
91
+ * defect. Its silence is not evidence the defect was small.
92
+ * - The corpus itself grew 677 → 678 `SKILL.md` between the two measurement
93
+ * dates; the delta above is A/B on the SAME 678 files, so it is a property of
94
+ * the detector rather than of the corpus.
95
+ *
96
+ * All 11 occurrences were read: every one names a tool bare that the same
97
+ * document qualifies in its own prose. Zero false positives.
98
+ *
99
+ * The SHIPPED command was re-run on the same corpus rather than trusted to
100
+ * agree with the probe — `vat audit --user`, 852 skills scanned, reproducing
101
+ * the installed-skills row occurrence for occurrence and line for line (plus
102
+ * the 4 linked-file occurrences `docs/validation-codes.md` records, which the
103
+ * probe above does not traverse).
104
+ *
105
+ * The frontmatter-including detector, run over the same 678 documents, reports
106
+ * 19 occurrences from the same 7 documents. The extra 8 are four copies of one
107
+ * skill naming `create_repository` and `create_branch` bare, whose only
108
+ * qualified spelling is its `allowed-tools:` frontmatter.
109
+ *
110
+ * ⛔ Those 8 WERE shipped. This paragraph used to claim the SKILL.md lane had
111
+ * "always passed a frontmatter-stripped slice" and that the 19 therefore
112
+ * described the probe rather than the product. `parseFileCached(...).content` is
113
+ * the raw file, frontmatter and all, so the shipped lane emitted every one of
114
+ * them. The 19 → 11 delta is a real behaviour change this detector made, not a
115
+ * correction to a probe.
116
+ *
117
+ * `warning`, not `error`: with a single MCP server mounted the bare name
118
+ * usually resolves, so the skill is degraded rather than broken.
119
+ */
120
+ import { CODE_REGISTRY } from '@vibe-agent-toolkit/schema';
121
+ /**
122
+ * `mcp__<server>__<tool>` — the Claude Code spelling.
123
+ *
124
+ * The server segment is non-greedy, so the FIRST `__` after it closes the server
125
+ * half and everything to its right is the tool. `mcp__plugin_github_github__get_me`
126
+ * therefore yields `get_me`, and `mcp__zapier__gmail__send_email` yields
127
+ * `gmail__send_email` — the tool half a document that writes that name bare will
128
+ * write.
129
+ *
130
+ * ⚠️ This comment used to say the LAST `__` was the separator, and to credit the
131
+ * non-greedy quantifier for it. Non-greedy stops at the FIRST. FIRST is also the
132
+ * behaviour to want: Claude Code joins server segments with SINGLE underscores
133
+ * (`plugin_<plugin>_<server>`), so the first `__` after `mcp__` closes the server
134
+ * half — checked against a live session's whole mounted-tool roster, 200-plus
135
+ * names, not one of which carries `__` inside its server half.
136
+ * Nothing caught the false claim because no fixture could: the one that pinned
137
+ * this line was `mcp__plugin_github_github__get_me`, whose server half has only
138
+ * single underscores, so its first and last `__` are the same character.
139
+ *
140
+ * Both halves admit `-`. `\w` does not, and excluding it made two whole
141
+ * families invisible:
142
+ *
143
+ * - **Hyphenated SERVER names matched nothing at all.** Claude Code mounts
144
+ * plugin servers as `plugin_<plugin>_<server>` and hyphenated plugin names are
145
+ * the norm (`mcp__plugin_microsoft-docs_microsoft-learn__microsoft_docs_search`,
146
+ * `mcp__claude-in-chrome__browser_batch`). A document driving MCP solely
147
+ * through such a server had an empty vocabulary and could produce no finding,
148
+ * true ones included.
149
+ * - **Hyphenated TOOL names truncated at the hyphen**, and `\b` was satisfied
150
+ * there, so `mcp__plugin_context7_context7__resolve-library-id` put `resolve`
151
+ * into the vocabulary. Every later code span spelling that ordinary English
152
+ * word — or `query`, from `query-docs` — became a finding.
153
+ *
154
+ * ⛔ **The tool capture must NOT end on `\b`, and camelCase is why.** It used to,
155
+ * and the consequence was not truncation — it was total invisibility. `\b`
156
+ * asserts a word/non-word boundary; when the character after the captured run is
157
+ * an UPPERCASE letter, both sides are word characters and the assertion fails.
158
+ * The engine then backtracks the lowercase class one character at a time, and
159
+ * every interior position is word|word too, so the whole match is abandoned:
160
+ *
161
+ * ```
162
+ * mcp__linear__createIssue → [] (not ["create"] — nothing at all)
163
+ * mcp__x__get_userInfo → []
164
+ * ```
165
+ *
166
+ * camelCase tool names are the ordinary case for MCP servers written in
167
+ * TypeScript, so a document driving such a server had an EMPTY vocabulary and
168
+ * could report nothing — the exact defect this detector exists to catch, in the
169
+ * exact documents most likely to have it. The hyphen family survived only
170
+ * because `-` is a non-word character, which is why a fixture existed for
171
+ * `foo-Bar` and none for `fooBar`.
172
+ *
173
+ * The fix is `(?![\w-])` — "not followed by a name character" — with the capture
174
+ * class widened to admit uppercase so the whole camelCase name is taken. A
175
+ * lookahead cannot be satisfied by giving back characters the way `\b` was, so
176
+ * there is no silent-abandonment mode left.
177
+ */
178
+ const CLAUDE_CODE_QUALIFIED = /\bmcp__[\w-]+?__([a-z][A-Za-z0-9_-]*)(?![\w-])/gu;
179
+ /**
180
+ * `ServerName:tool_name` — the API spelling. Deliberately shapeless: two flat
181
+ * classes either side of a literal `:`, with every *judgement* about the halves
182
+ * made in code by {@link SERVER_IS_NAMED} and {@link isMultiSegmentToolName}.
183
+ *
184
+ * The natural spelling encodes both judgements inline —
185
+ * `(?=[A-Za-z0-9-]*[A-Z])[A-Za-z][A-Za-z0-9-]*:([a-z][a-z0-9_]*_[a-z0-9_]+)` —
186
+ * and `sonarjs/super-linear-regex` rejects it for backtracking: `[A-Za-z]`
187
+ * overlaps the `[\w-]*` behind it, and `[a-z0-9_]*_` is ambiguous about which
188
+ * `_` is the literal one. The flat form removes both, which is why it passes.
189
+ *
190
+ * ⚠️ **The rewrite bought nothing at runtime, and the comment here used to claim
191
+ * otherwise.** Measured on 20k → 80k character inputs (a 4x input, so linear
192
+ * predicts ~4x and quadratic ~16x):
193
+ *
194
+ * | Spelling | one colonless token | hyphenated run |
195
+ * |---|---|---|
196
+ * | this flat form | 0.05 → 0.22 ms (**4.0x**) | 0.06 → 0.21 ms (**3.3x**) |
197
+ * | the natural form sonarjs rejects | 0.06 → 0.24 ms (**3.9x**) | 0.05 → 0.14 ms (**2.9x**) |
198
+ * | flat form, lookbehind removed | 154 → **2595 ms** (**16.8x**) | 211 → **2879 ms** (**13.6x**) |
199
+ *
200
+ * So the linter was wrong about the natural form — it was already linear — and
201
+ * the flat spelling is a readability-and-linter change, not a fix. What actually
202
+ * decides the complexity is the `(?<![\w-])` **lookbehind**, and it is the one
203
+ * piece here that must not be touched: without it the engine restarts the scan at
204
+ * EVERY character of a long token that has no colon, and 80 KB of prose costs 2.6
205
+ * seconds. Same mechanism, and the same measured shape, as the lookbehind on
206
+ * `INLINE_LINK_REGEX` in `post-build-checks.ts`.
207
+ *
208
+ * A test holds the catastrophic case out rather than this comment, because a
209
+ * regex can be rewritten into a shape a checker likes while staying quadratic —
210
+ * and, as the table shows, flagged while already being linear. Neither direction
211
+ * is observable from the linter's verdict. That test asserts a fixed budget on
212
+ * one input size, not a growth ratio: the two columns above are four orders of
213
+ * magnitude apart, so an absolute gate separates them, whereas a ratio needs a
214
+ * denominator too small to measure honestly on a fast machine.
215
+ *
216
+ * ⛔ The tool capture ends on `(?![\w-])`, not `\b`, and for the same reason
217
+ * {@link CLAUDE_CODE_QUALIFIED} does: `([a-z0-9_]+)\b` matched `GitHub:createIssue`
218
+ * NOT AT ALL — not as `create`, not at all — because `\b` fails between `e` and
219
+ * `I`, and every position the engine backtracks to is word|word as well. The
220
+ * capture also opens with `[a-z]` rather than `[a-z0-9_]`, which changes no
221
+ * outcome ({@link isMultiSegmentToolName} already demanded a lowercase first
222
+ * character) and lets the regex say what it means.
223
+ */
224
+ const API_QUALIFIED = /(?<![\w-])([\w-]+):([a-z]\w*)(?![\w-])/gu;
225
+ /**
226
+ * A server half is an MCP server name only if it is spelled the way Anthropic's
227
+ * own examples spell one: it STARTS with an uppercase letter and carries no
228
+ * underscore — `GitHub:`, `BigQuery:`. This one predicate is what excludes
229
+ * `node:child_process` and `whiteboard:read:list_whiteboards`; see the module
230
+ * docstring for where each was measured.
231
+ *
232
+ * ⚠️ It used to be `/[A-Z]/` — an uppercase letter ANYWHERE — and a reviewer
233
+ * flagged that as too loose. Tightening to the documented shape was measured
234
+ * cost-free: over 884 documents (206 authoring-project `.md` + 678 installed
235
+ * `SKILL.md`) the loose rule accepted exactly three distinct server halves —
236
+ * `GitHub`, `BigQuery` and the literal `ServerName` of the guidance's own
237
+ * template — and the strict rule accepts all three. It rejects nothing that was
238
+ * ever observed.
239
+ *
240
+ * ⛔ Be honest about what it does NOT buy. The reviewer's own example,
241
+ * `Note:this_is_fine`, satisfies BOTH halves of the strict rule and still seeds
242
+ * the vocabulary. This is a narrowing toward the documented spelling, not a fix
243
+ * for that class — no cheap predicate separates a prose `Word:token` from a
244
+ * genuine `Server:tool`, and the reviewer could construct no realistic markdown
245
+ * instance because the pattern admits no whitespace after the colon, which rules
246
+ * out prose, YAML and JSON alike.
247
+ */
248
+ const SERVER_IS_NAMED = /^[A-Z][A-Za-z0-9-]*$/u;
249
+ /** A lowercase letter opens every MCP tool name. */
250
+ const TOOL_STARTS_LOWERCASE = /^[a-z]/u;
251
+ /**
252
+ * A camelCase hump — a lowercase-or-digit immediately followed by an uppercase
253
+ * letter. `addCaseLinks`, `createIssue` and `get_userInfo` all carry one; `find`
254
+ * and `resolve` do not. See {@link isMultiSegmentToolName} for why that matters.
255
+ */
256
+ const TOOL_HAS_CASE_HUMP = /[a-z0-9][A-Z]/u;
257
+ /**
258
+ * Separators the greedy tool capture can end on — when the character after the
259
+ * name is neither a word character nor a hyphen, so `` `mcp__x__do_thing_` ``
260
+ * captures `do_thing_`.
261
+ *
262
+ * ⚠️ This used to say "when the name is followed by an uppercase letter
263
+ * (`mcp__x__foo-Bar` captures `foo-`)". That was true only while the capture
264
+ * ended on `\b` and excluded uppercase, which is the same defect that made every
265
+ * camelCase tool name invisible; see {@link CLAUDE_CODE_QUALIFIED}. That name
266
+ * now captures whole, as `foo-Bar`.
267
+ */
268
+ const TOOL_SEPARATORS = new Set(['-', '_']);
269
+ /**
270
+ * The capture without those trailing separators.
271
+ *
272
+ * Still reachable after the camelCase fix, on a different shape: a trailing
273
+ * separator followed by a non-name character, as in `` `mcp__x__do_thing_` ``.
274
+ *
275
+ * Trimmed in code, twice over. Requiring a non-separator last character inside
276
+ * {@link CLAUDE_CODE_QUALIFIED} costs a nested quantifier for a case handled here
277
+ * in one pass; and the obvious `.replace(/[-_]+$/u, '')` is itself super-linear —
278
+ * `sonarjs/super-linear-regex` rejects it, correctly, because a long run of
279
+ * separators makes every position a candidate start. A backwards walk has
280
+ * nothing to backtrack.
281
+ */
282
+ function withoutTrailingSeparators(tool) {
283
+ let end = tool.length;
284
+ while (end > 0 && TOOL_SEPARATORS.has(tool[end - 1] ?? ''))
285
+ end--;
286
+ return tool.slice(0, end);
287
+ }
288
+ /**
289
+ * A tool half opens with a lowercase letter and is MULTI-SEGMENT — it carries an
290
+ * underscore, a hyphen, or a camelCase hump.
291
+ *
292
+ * ⚠️ **The hump is not decoration; without it the camelCase fix buys nothing.**
293
+ * `addCaseLinks` and `createIssue` contain neither `_` nor `-`, so a gate written
294
+ * as "contains a separator" refuses every camelCase tool name — the vocabulary
295
+ * stays empty and the regex fix is inert. The predicate the gate is really
296
+ * asking is *"is this a NAME rather than an ordinary English word?"*, and a case
297
+ * boundary answers it exactly as well as a separator does: `find` is a word,
298
+ * `addCaseLinks` is a name. That the two families are spelled differently is a
299
+ * fact about the server's implementation language, not about ambiguity.
300
+ *
301
+ * Applied to BOTH spellings; see {@link qualifiedNamesIn}. Gating only the API
302
+ * form let the `mcp__` form contribute ONE-WORD names to the vocabulary, so
303
+ * `mcp__claude-in-chrome__find` seeded `find` and every later code span spelling
304
+ * that ordinary English word became a finding. Same family as the uppercase-server
305
+ * rule, and not closed by the hyphen fix: that one stopped a name being TRUNCATED
306
+ * at its hyphen, this one stops a name that is one word to begin with.
307
+ *
308
+ * ⚠️ The separator set is `_` OR `-`, not `_` alone. Requiring an underscore
309
+ * would drop `resolve-library-id` and `query-docs` — real, kebab-spelled MCP
310
+ * tools whose whole capture the hyphen fix exists to preserve. What earns a place
311
+ * in the vocabulary is being multi-word, because that is what makes a bare
312
+ * occurrence unambiguous; `find` is a word, `resolve-library-id` is a name. The
313
+ * API lane's capture holds no hyphen, so widening the predicate to admit one
314
+ * leaves that lane exactly as it was.
315
+ *
316
+ * Written as a predicate rather than the obvious `^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$`
317
+ * because that spelling nests a quantifier inside a quantifier, which
318
+ * `security/detect-unsafe-regex` flags as a ReDoS shape. The captures this runs
319
+ * on are already character-class-constrained, so the only things left to decide
320
+ * are the first character and the presence of a separator — and `String.includes`
321
+ * decides that in linear time with nothing to backtrack.
322
+ */
323
+ function isMultiSegmentToolName(tool) {
324
+ if (!TOOL_STARTS_LOWERCASE.test(tool))
325
+ return false;
326
+ return tool.includes('_') || tool.includes('-') || TOOL_HAS_CASE_HUMP.test(tool);
327
+ }
328
+ /** Inline code spans. Newline-free so a runaway backtick cannot swallow a paragraph. */
329
+ const CODE_SPAN = /`([^`\n]+)`/g;
330
+ /** The fence that opens and closes YAML frontmatter. */
331
+ const FRONTMATTER_FENCE = '---';
332
+ /**
333
+ * Index one past the closing frontmatter fence, or 0 when `lines` opens no
334
+ * frontmatter. An unterminated opening fence is not frontmatter: a document
335
+ * whose first line happens to be a horizontal rule keeps all of its body.
336
+ */
337
+ function frontmatterEnd(lines) {
338
+ if (lines[0]?.trim() !== FRONTMATTER_FENCE)
339
+ return 0;
340
+ for (let index = 1; index < lines.length; index++) {
341
+ if (lines[index]?.trim() === FRONTMATTER_FENCE)
342
+ return index + 1;
343
+ }
344
+ return 0;
345
+ }
346
+ /**
347
+ * `content` split into lines, with any leading YAML frontmatter blanked rather
348
+ * than removed — every reported `line` stays the line the author will open the
349
+ * file to, which dropping the lines outright would silently shift.
350
+ */
351
+ function scannableLines(content) {
352
+ const lines = content.split('\n');
353
+ const end = frontmatterEnd(lines);
354
+ for (let index = 0; index < end; index++)
355
+ lines[index] = '';
356
+ return lines;
357
+ }
358
+ /** Every MCP tool name `text` spells fully-qualified, in either spelling. */
359
+ function qualifiedNamesIn(text) {
360
+ const vocabulary = new Set();
361
+ for (const match of text.matchAll(CLAUDE_CODE_QUALIFIED)) {
362
+ const tool = withoutTrailingSeparators(match[1] ?? '');
363
+ // The SAME multi-segment gate the API lane applies, and for the same reason.
364
+ // It used to sit on one lane only: `API_QUALIFIED` demanded an underscore
365
+ // while `mcp__…` demanded nothing, so `mcp__claude-in-chrome__find` seeded
366
+ // the vocabulary with `find` and every later code span spelling that ordinary
367
+ // word became a finding. Structurally the same class as the hyphen bug
368
+ // (`resolve-library-id` → `resolve`), which the hyphen fix does not close —
369
+ // that one truncated a name, this one admits a name that is one word to begin
370
+ // with. The cost is that a genuinely single-word MCP tool spelled bare goes
371
+ // unreported, which is the safe direction for a warning whose whole argument
372
+ // is precision.
373
+ if (tool !== '' && isMultiSegmentToolName(tool))
374
+ vocabulary.add(tool);
375
+ }
376
+ for (const match of text.matchAll(API_QUALIFIED)) {
377
+ const [, server, tool] = match;
378
+ if (server === undefined || tool === undefined)
379
+ continue;
380
+ if (!SERVER_IS_NAMED.test(server) || !isMultiSegmentToolName(tool))
381
+ continue;
382
+ vocabulary.add(tool);
383
+ }
384
+ return vocabulary;
385
+ }
386
+ /**
387
+ * Every MCP tool name `content` spells fully-qualified in its body, in either
388
+ * spelling. Frontmatter supplies nothing; see the module docstring.
389
+ *
390
+ * Exported for tests: this set IS the detector's premise, and pinning it directly
391
+ * is cheaper and clearer than inferring it from emitted issues.
392
+ */
393
+ export function qualifiedMcpToolNames(content) {
394
+ return qualifiedNamesIn(scannableLines(content).join('\n'));
395
+ }
396
+ /**
397
+ * Emit one `MCP_TOOL_NAME_UNQUALIFIED` per (line, bare tool name).
398
+ *
399
+ * Per instance rather than per document, each carrying the bare tool name as
400
+ * `link`, because `applyAllowFilter` matches an allow glob against `location` OR
401
+ * `link`. That is what lets an adopter waive one identifier:
402
+ *
403
+ * ```yaml
404
+ * validation:
405
+ * allow:
406
+ * MCP_TOOL_NAME_UNQUALIFIED:
407
+ * - paths: ["get_me"]
408
+ * reason: "Named bare in the availability-probe step on purpose; the
409
+ * qualified form is three lines above."
410
+ * ```
411
+ *
412
+ * …while a different bare tool name in the same document still fires. Waiver
413
+ * granularity is the emitter's decision, not the allow machinery's.
414
+ *
415
+ * Only inline code spans count. An agent copies what is in a code span; the same
416
+ * word in running prose is discussion of the tool rather than an instruction to
417
+ * call it. This is a design choice, not a measured one — the corpus numbers in
418
+ * the module docstring were all taken with the code-span rule already applied, so
419
+ * nothing here says what admitting prose would have cost.
420
+ *
421
+ * @param content Raw markdown of one skill document, frontmatter and all.
422
+ * @param docLocation Project-relative path of that document, for the anchor.
423
+ */
424
+ export function collectUnqualifiedMcpToolIssues(content, docLocation, issues) {
425
+ const lines = scannableLines(content);
426
+ const vocabulary = qualifiedNamesIn(lines.join('\n'));
427
+ if (vocabulary.size === 0)
428
+ return;
429
+ const registryEntry = CODE_REGISTRY.MCP_TOOL_NAME_UNQUALIFIED;
430
+ for (const [index, line] of lines.entries()) {
431
+ const seenOnLine = new Set();
432
+ // Computed only once a span has already hit the vocabulary, so an ordinary
433
+ // line pays nothing for the exemption below.
434
+ let qualifiedOnLine;
435
+ for (const match of line.matchAll(CODE_SPAN)) {
436
+ const inner = (match[1] ?? '').trim();
437
+ if (!vocabulary.has(inner) || seenOnLine.has(inner))
438
+ continue;
439
+ // A line that spells THIS tool fully-qualified is the definition the
440
+ // vocabulary was built from — a "`get_me` — that is `GitHub:get_me`"
441
+ // gloss, or a row of a bare-to-qualified mapping table. Reporting it
442
+ // would flag the very thing this code asks authors to write.
443
+ //
444
+ // Per MATCH, not per line: a line that qualifies tool A while naming
445
+ // tool B bare is exactly the defect this check exists to report, and a
446
+ // per-line skip dropped it without a trace.
447
+ qualifiedOnLine ??= qualifiedNamesIn(line);
448
+ if (qualifiedOnLine.has(inner))
449
+ continue;
450
+ seenOnLine.add(inner);
451
+ issues.push({
452
+ severity: registryEntry.defaultSeverity,
453
+ code: 'MCP_TOOL_NAME_UNQUALIFIED',
454
+ message: `MCP tool "${inner}" is named without its server prefix; this document ` +
455
+ `spells it fully-qualified elsewhere`,
456
+ location: docLocation,
457
+ line: index + 1,
458
+ // The tool name, so one identifier can be waived without silencing the
459
+ // document. Never the location: a tool name is not a file to open.
460
+ link: inner,
461
+ fix: registryEntry.fix,
462
+ reference: registryEntry.reference,
463
+ });
464
+ }
465
+ }
466
+ }
467
+ //# sourceMappingURL=mcp-tool-qualification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-tool-qualification.js","sourceRoot":"","sources":["../../src/validators/mcp-tool-qualification.ts"],"names":[],"mappings":"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"}