tidewave 0.4.1 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3375b13a00fb5b052a2bea5c6b720056379f16a26cc984ffde2973d2d2fca90
4
- data.tar.gz: b5d7c9d2290e2273116712116fcb8336ddbc4009be187ca0413ec3c727b33e6b
3
+ metadata.gz: d75b5659655095637ad8830051ef50f06e018339520d093c51854cec9c320fd7
4
+ data.tar.gz: 9325b6a4ea7ac4e1444a677bd1dc785bc6f8046e8492a5d1b70267c9424ca9a3
5
5
  SHA512:
6
- metadata.gz: 6ad6d175fef2e61d40ab97de1bbf36bfaf3f37137ee7d54d6cc1f7987096e105e37381376e6f8263b70b31795065824b459225512d62e9ae5615c9dc098a8b2d
7
- data.tar.gz: 2535c2570a69f70114705b2ec13f708b17bae00aefcaae504c97323c2c07edb3686a142d40f6a85f42a7619754f93fb8cbcd7267edcbb349fc621004f15c3ae2
6
+ metadata.gz: 9fe7d353f945a3a3735dfdd56486188df12034736c2ac78561dca478b05b6f15fba9608dd8e8209f3d9a66659f4908c2814ec65ccdf13030f56b1387a9190fde
7
+ data.tar.gz: 6782f79c55813d99d5cffeb973812116aaf581570119de0ebcd739d2f65d1aa0fbc3ed3bc1cc1d478ec0b30ac18fbc2ab9db6037df18ec433b8ef50cce7eb947
data/README.md CHANGED
@@ -2,11 +2,17 @@
2
2
 
3
3
  Tidewave is the coding agent for full-stack web app development. Integrate Claude Code, OpenAI Codex, and other agents with your web app and web framework at every layer, from UI to database. [See our website](https://tidewave.ai) for more information.
4
4
 
5
- This project can also be used as a standalone Model Context Protocol server for your editors.
5
+ This project can also be used as [a standalone Model Context Protocol server](https://hexdocs.pm/tidewave/mcp.html).
6
6
 
7
7
  ## Installation
8
8
 
9
- You can install Tidewave by adding the `tidewave` gem to the development group in your Gemfile:
9
+ You can install Tidewave by running:
10
+
11
+ ```shell
12
+ bundle add tidewave --group development
13
+ ```
14
+
15
+ or by manully adding the `tidewave` gem to the development group in your Gemfile:
10
16
 
11
17
  ```ruby
12
18
  gem "tidewave", group: :development
@@ -14,30 +20,41 @@ gem "tidewave", group: :development
14
20
 
15
21
  Now make sure [Tidewave is installed](https://hexdocs.pm/tidewave/installation.html) and you are ready to connect Tidewave to your app.
16
22
 
23
+ ## Development
24
+
25
+ Run the Minitest suite with:
26
+
27
+ ```shell
28
+ bundle exec ruby -Itest test/all_test.rb
29
+ ```
30
+
17
31
  ## Troubleshooting
18
32
 
19
- ### Content security policy
33
+ ### Using multiple hosts/subdomains
20
34
 
21
- If you have enabled Content-Security-Policy, Tidewave will automatically enable "unsafe-eval" under `script-src` in order for contextual browser testing to work correctly. It also disables the `frame-ancestors` directive.
35
+ If you are using multiple hosts/subdomains during development, you must use `*.localhost`, as such domains are considered secure by browsers. Additionally, add the following to `config/initializers/development.rb`:
22
36
 
23
- ### Production Environment
37
+ ```ruby
38
+ config.session_store :cookie_store,
39
+ key: "__your_app_session",
40
+ same_site: :none,
41
+ secure: true,
42
+ assume_ssl: true
43
+ ```
24
44
 
25
- Tidewave is a powerful tool that can help you develop your web application faster and more efficiently. However, it is important to note that Tidewave is not meant to be used in a production environment.
45
+ And make sure you are using `rack-session` version `2.1.0` or later.
26
46
 
27
- Tidewave will raise an error if it is used in any environment where code reloading is disabled (which typically includes production).
47
+ The above will allow your application to run embedded within Tidewave across multiple subdomains, as long as it is using a secure context (such as `admin.localhost`, `www.foobar.localhost`, etc).
28
48
 
29
- ### Localhost requirement
49
+ ### Content security policy
30
50
 
31
- > This requirement only matters if you are not using the Tidewave app/CLI.
51
+ If you have enabled Content-Security-Policy, Tidewave will automatically enable "unsafe-eval" under `script-src` in order for contextual browser testing to work correctly. It also disables the `frame-ancestors` directive.
32
52
 
33
- Tidewave expects your web application to be running on `localhost`. If you are not running on localhost, you may need to set some additional configuration. In particular, you must configure Tidewave to allow `allow_remote_access` and [optionally configure your Rails hosts](https://guides.rubyonrails.org/configuring.html#actiondispatch-hostauthorization). For example, in your `config/environments/development.rb`:
53
+ ### Production Environment
34
54
 
35
- ```ruby
36
- config.hosts << "company.local"
37
- config.tidewave.allow_remote_access = true
38
- ```
55
+ Tidewave is a powerful tool that can help you develop your web application faster and more efficiently. However, it is important to note that Tidewave is not meant to be used in a production environment.
39
56
 
40
- If you want to use Docker for development, you either need to enable the configuration above or automatically redirect the relevant ports, as done by [devcontainers](https://code.visualstudio.com/docs/devcontainers/containers). See our [containers](https://hexdocs.pm/tidewave/containers.html) guide for more information.
57
+ Tidewave will raise an error if it is used in any environment where code reloading is disabled (which typically includes production).
41
58
 
42
59
  ## Configuration
43
60
 
@@ -49,7 +66,7 @@ You may configure `tidewave` using the following syntax:
49
66
 
50
67
  The following config is available:
51
68
 
52
- * `allow_remote_access` - Tidewave only allows requests from localhost by default, even if your server listens on other interfaces. If you trust your network and need to access Tidewave from a different machine, this configuration can be set to `true`
69
+ * `allow_remote_access` - Tidewave only allows requests from localhost by default, even if your server listens on other interfaces, for security purposes. Read [our security guidelines for more information and when to allow remote access](https://hexdocs.pm/tidewave/security.html) (if you know what you are doing)
53
70
 
54
71
  * `logger_middleware` - The logger middleware Tidewave should wrap to silence its own logs
55
72
 
@@ -57,9 +74,29 @@ The following config is available:
57
74
 
58
75
  * `team` - set your Tidewave Team configuration, such as `config.tidewave.team = { id: "my-company" }`
59
76
 
77
+ ## Available tools
78
+
79
+ - `execute_sql_query` - executes a SQL query within your application
80
+ database, useful for the agent to verify the result of an action
81
+
82
+ - `get_docs` - get the documentation for a given module/class/method.
83
+ It consults the exact versions used by the project, ensuring you always
84
+ get correct information
85
+
86
+ - `get_logs` - reads logs written by the server
87
+
88
+ - `get_models` - lists all modules in the application and their location
89
+ for quick discovery
90
+
91
+ - `get_source_location` - get the source location for a given module/class/method,
92
+ so an agent can directly read the source skipping search
93
+
94
+ - `project_eval` - evaluates code within the Rails application itself, giving the agent
95
+ access to your runtime, dependencies, and in-memory data
96
+
60
97
  ## Acknowledgements
61
98
 
62
- A thank you to Yorick Jacquin, for creating [FastMCP](https://github.com/yjacquin/fast_mcp) and implementing the initial version of this project.
99
+ A thank you to Yorick Jacquin for the initial version of this project.
63
100
 
64
101
  ## License
65
102
 
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tidewave
3
+ class Tidewave
4
4
  class Configuration
5
5
  attr_accessor :logger, :allow_remote_access, :preferred_orm, :dev, :client_url, :team, :logger_middleware
6
6
 
7
7
  def initialize
8
- @logger = nil
8
+ # Rails has a hosts middleware which already checks for this
9
9
  @allow_remote_access = true
10
+ @logger = nil
10
11
  @preferred_orm = :active_record
11
12
  @dev = false
12
13
  @client_url = "https://tidewave.ai"
@@ -1,14 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tidewave
3
+ class Tidewave
4
4
  class DatabaseAdapter
5
5
  class << self
6
- def current
7
- @current ||= create_adapter
8
- end
9
-
10
- def create_adapter
11
- orm_type = Rails.application.config.tidewave.preferred_orm
6
+ def for(orm_type)
12
7
  case orm_type
13
8
  when :active_record
14
9
  require_relative "database_adapters/active_record"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tidewave
3
+ class Tidewave
4
4
  module DatabaseAdapters
5
5
  class ActiveRecord < DatabaseAdapter
6
6
  RESULT_LIMIT = 50
@@ -21,19 +21,13 @@ module Tidewave
21
21
  rows: result.rows.first(RESULT_LIMIT),
22
22
  row_count: result.rows.length,
23
23
  adapter: conn.adapter_name,
24
- database: database_name
24
+ database: conn.pool.db_config.database
25
25
  }
26
26
  end
27
27
 
28
28
  def get_models
29
29
  ::ActiveRecord::Base.descendants
30
30
  end
31
-
32
- private
33
-
34
- def database_name
35
- Rails.configuration.database_configuration.dig(Rails.env, "database")
36
- end
37
31
  end
38
32
  end
39
33
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tidewave
3
+ class Tidewave
4
4
  module DatabaseAdapters
5
5
  class Sequel < DatabaseAdapter
6
6
  RESULT_LIMIT = 50
@@ -31,7 +31,19 @@ module Tidewave
31
31
 
32
32
  def get_models
33
33
  # Filter out anonymous Sequel models that can't be resolved as constants
34
- ::Sequel::Model.descendants.reject { |model| model.name&.start_with?("Sequel::_Model(") }
34
+ descendants_of(::Sequel::Model).reject { |model| model.name&.start_with?("Sequel::_Model(") }
35
+ end
36
+
37
+ private
38
+
39
+ def descendants_of(base)
40
+ if base.respond_to?(:descendants)
41
+ base.descendants
42
+ elsif base.respond_to?(:subclasses)
43
+ base.subclasses.flat_map { |subclass| [ subclass ] + descendants_of(subclass) }
44
+ else
45
+ []
46
+ end
35
47
  end
36
48
  end
37
49
  end
@@ -1,37 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "logger"
4
- require "fileutils"
5
4
  require "tidewave/configuration"
6
- require "tidewave/middleware"
7
5
  require "tidewave/exceptions_middleware"
8
6
  require "tidewave/quiet_requests_middleware"
9
7
 
10
- gem_tools_path = File.expand_path("tools/**/*.rb", __dir__)
11
- Dir[gem_tools_path].each { |f| require f }
12
-
13
- # Temporary monkey patching to address regression in FastMCP
14
- if Dry::Schema::Macros::Hash.method_defined?(:original_call)
15
- Dry::Schema::Macros::Hash.class_eval do
16
- def call(*args, &block)
17
- if block
18
- # Use current context to track nested context if available
19
- context = MetadataContext.current
20
- if context
21
- context.with_nested(name) do
22
- original_call(*args, &block)
23
- end
24
- else
25
- original_call(*args, &block)
26
- end
27
- else
28
- original_call(*args)
29
- end
30
- end
31
- end
32
- end
33
-
34
- module Tidewave
8
+ class Tidewave
35
9
  class Railtie < Rails::Railtie
36
10
  config.tidewave = Tidewave::Configuration.new()
37
11
 
@@ -40,10 +14,21 @@ module Tidewave
40
14
  raise "For security reasons, Tidewave is only supported in environments where config.enable_reloading is true (typically development)"
41
15
  end
42
16
 
17
+ tidewave_config = app.config.tidewave
18
+
43
19
  app.config.middleware.insert_after(
44
20
  ActionDispatch::Callbacks,
45
- Tidewave::Middleware,
46
- app.config.tidewave
21
+ Tidewave,
22
+ allow_remote_access: tidewave_config.allow_remote_access,
23
+ client_url: tidewave_config.client_url,
24
+ framework_type: "rails",
25
+ project_name: app.class.module_parent.name,
26
+ team: tidewave_config.team,
27
+ logger: tidewave_config.logger || Rails.logger,
28
+ root: Rails.root,
29
+ log_file: Rails.root.join("log", "#{Rails.env}.log"),
30
+ orm_adapter: tidewave_config.preferred_orm,
31
+ before_reload: -> { app.eager_load! }
47
32
  )
48
33
 
49
34
  app.config.after_initialize do
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tidewave
4
+ class Tool
5
+ class << self
6
+ def descendants
7
+ @descendants ||= []
8
+ end
9
+
10
+ def inherited(subclass)
11
+ descendants << subclass
12
+ super
13
+ end
14
+ end
15
+
16
+ def initialize(_options = {})
17
+ end
18
+
19
+ def definition
20
+ raise NotImplementedError, "#{self.class} must implement #definition"
21
+ end
22
+
23
+ def call(_arguments = {})
24
+ raise NotImplementedError, "#{self.class} must implement #call"
25
+ end
26
+
27
+ def validate_and_call(arguments)
28
+ arguments ||= {}
29
+
30
+ unless arguments.is_a?(Hash)
31
+ raise ArgumentError, "Invalid arguments: expected an object"
32
+ end
33
+
34
+ # This validator intentionally enforces only a small subset of JSON Schema:
35
+ # `type`, `required`, and scalar `default` values. Other keywords such as
36
+ # `minLength`, `maxLength`, `enum`, and `pattern` remain descriptive until
37
+ # Tidewave grows broader schema support.
38
+ validate_schema(arguments, definition.fetch("inputSchema", {}))
39
+ call(arguments)
40
+ end
41
+
42
+ private
43
+
44
+ def validate_schema(value, schema, path = nil)
45
+ return unless schema.is_a?(Hash)
46
+
47
+ validate_type(value, schema["type"], path) if schema["type"]
48
+
49
+ case schema["type"]
50
+ when "object"
51
+ validate_object(value, schema, path)
52
+ when "array"
53
+ validate_array(value, schema, path)
54
+ end
55
+ end
56
+
57
+ def validate_object(value, schema, path)
58
+ properties = schema.fetch("properties", {})
59
+
60
+ properties.each do |name, property_schema|
61
+ if !value.key?(name) && property_schema.is_a?(Hash) && property_schema.key?("default")
62
+ validate_default(property_schema["default"], property_path(path, name))
63
+ value[name] = property_schema["default"]
64
+ end
65
+
66
+ next unless value.key?(name)
67
+
68
+ validate_schema(value[name], property_schema, property_path(path, name))
69
+ end
70
+
71
+ validate_required_properties(value, schema.fetch("required", []), path)
72
+ end
73
+
74
+ def validate_array(value, schema, path)
75
+ item_schema = schema["items"]
76
+ return unless item_schema.is_a?(Hash) && !item_schema.empty?
77
+
78
+ value.each_with_index do |item, index|
79
+ validate_schema(item, item_schema, "#{path || 'value'}[#{index}]")
80
+ end
81
+ end
82
+
83
+ def validate_required_properties(value, required_properties, path)
84
+ required_properties.each do |name|
85
+ next if value.key?(name)
86
+
87
+ raise ArgumentError, "Invalid arguments: missing required property '#{property_path(path, name)}'"
88
+ end
89
+ end
90
+
91
+ def validate_type(value, type, path)
92
+ return if value_matches_type?(value, type)
93
+
94
+ raise ArgumentError, "Invalid arguments: property '#{path || 'value'}' must be #{article_for(type)} #{type}"
95
+ end
96
+
97
+ def value_matches_type?(value, type)
98
+ case type
99
+ when "object"
100
+ value.is_a?(Hash)
101
+ when "array"
102
+ value.is_a?(Array)
103
+ when "string"
104
+ value.is_a?(String)
105
+ when "integer"
106
+ value.is_a?(Integer)
107
+ when "boolean"
108
+ value == true || value == false
109
+ else
110
+ true
111
+ end
112
+ end
113
+
114
+ def property_path(path, name)
115
+ path ? "#{path}.#{name}" : name
116
+ end
117
+
118
+ def article_for(type)
119
+ %w[array integer object].include?(type) ? "an" : "a"
120
+ end
121
+
122
+ def validate_default(value, path)
123
+ return unless value.is_a?(Hash) || value.is_a?(Array)
124
+
125
+ raise ArgumentError, "Invalid tool definition: property '#{path}' cannot use an object or array default"
126
+ end
127
+ end
128
+ end
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Tidewave::Tools::ExecuteSqlQuery < Tidewave::Tools::Base
4
- tool_name "execute_sql_query"
5
- description <<~DESCRIPTION
3
+ class Tidewave::Tools::ExecuteSqlQuery < Tidewave::Tool
4
+ DESCRIPTION = <<~DESCRIPTION.freeze
6
5
  Executes the given SQL query against the database connection.
7
6
  Returns the result as a Ruby data structure.
8
7
 
@@ -17,20 +16,38 @@ class Tidewave::Tools::ExecuteSqlQuery < Tidewave::Tools::Base
17
16
  For MySQL, use ? for parameter placeholders.
18
17
  DESCRIPTION
19
18
 
20
- arguments do
21
- required(:query).filled(:string).description("The SQL query to execute. For PostgreSQL, use $1, $2 placeholders. For MySQL, use ? placeholders.")
22
- optional(:arguments).value(:array).description("The arguments to pass to the query. The query must contain corresponding parameter placeholders.")
19
+ def initialize(options = {})
20
+ @database_adapter = Tidewave::DatabaseAdapter.for(options[:orm_adapter]) if options[:orm_adapter]
23
21
  end
24
22
 
25
- def @input_schema.json_schema
26
- schema = super
27
- schema[:properties][:arguments][:items] = {}
28
- schema
23
+ def definition
24
+ return nil unless @database_adapter
25
+
26
+ {
27
+ "name" => "execute_sql_query",
28
+ "description" => DESCRIPTION,
29
+ "inputSchema" => {
30
+ "type" => "object",
31
+ "properties" => {
32
+ "query" => {
33
+ "type" => "string",
34
+ "minLength" => 1,
35
+ "description" => "The SQL query to execute. For PostgreSQL, use $1, $2 placeholders. For MySQL, use ? placeholders."
36
+ },
37
+ "arguments" => {
38
+ "type" => "array",
39
+ "items" => {},
40
+ "description" => "The arguments to pass to the query. The query must contain corresponding parameter placeholders."
41
+ }
42
+ },
43
+ "required" => [ "query" ]
44
+ }
45
+ }
29
46
  end
30
47
 
31
- RESULT_LIMIT = 50
32
-
33
- def call(query:, arguments: [])
34
- Tidewave::DatabaseAdapter.current.execute_query(query, arguments)
48
+ def call(arguments_hash)
49
+ query = arguments_hash.fetch("query")
50
+ arguments = arguments_hash.fetch("arguments", [])
51
+ @database_adapter.execute_query(query, arguments)
35
52
  end
36
53
  end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Tidewave::Tools::GetDocs < Tidewave::Tools::Base
4
- tool_name "get_docs"
5
-
6
- description <<~DESCRIPTION
3
+ class Tidewave::Tools::GetDocs < Tidewave::Tool
4
+ DESCRIPTION = <<~DESCRIPTION.freeze
7
5
  Returns the documentation for the given reference.
8
6
 
9
7
  The reference may be a constant, most commonly classes and modules
@@ -16,11 +14,26 @@ class Tidewave::Tools::GetDocs < Tidewave::Tools::Base
16
14
  If that is the case, prefer this tool over grepping the file system.
17
15
  DESCRIPTION
18
16
 
19
- arguments do
20
- required(:reference).filled(:string).description("The constant/method to lookup, such String, String#gsub or File.executable?")
17
+ def definition
18
+ {
19
+ "name" => "get_docs",
20
+ "description" => DESCRIPTION,
21
+ "inputSchema" => {
22
+ "type" => "object",
23
+ "properties" => {
24
+ "reference" => {
25
+ "type" => "string",
26
+ "minLength" => 1,
27
+ "description" => "The constant/method to lookup, such String, String#gsub or File.executable?"
28
+ }
29
+ },
30
+ "required" => [ "reference" ]
31
+ }
32
+ }
21
33
  end
22
34
 
23
- def call(reference:)
35
+ def call(arguments)
36
+ reference = arguments.fetch("reference")
24
37
  file_path, line_number = Tidewave::Tools::GetSourceLocation.get_source_location(reference)
25
38
 
26
39
  if file_path
@@ -47,7 +60,8 @@ class Tidewave::Tools::GetDocs < Tidewave::Tools::Base
47
60
  line = lines[current_line].chomp.strip
48
61
 
49
62
  if line.start_with?("#")
50
- comment_lines.unshift(line.sub(/^#\s|^#/, ""))
63
+ comment = line.sub(/^#\s|^#/, "")
64
+ comment_lines.unshift(comment) unless ignorable_comment?(comment)
51
65
  elsif line.empty?
52
66
  # Skip empty lines but continue looking for comments
53
67
  else
@@ -61,4 +75,8 @@ class Tidewave::Tools::GetDocs < Tidewave::Tools::Base
61
75
  return nil if comment_lines.empty?
62
76
  comment_lines.join("\n")
63
77
  end
78
+
79
+ def ignorable_comment?(comment)
80
+ comment.start_with?("rubocop:")
81
+ end
64
82
  end
@@ -1,26 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Tidewave::Tools::GetLogs < Tidewave::Tools::Base
4
- tool_name "get_logs"
5
- description <<~DESCRIPTION
3
+ require "pathname"
4
+
5
+ class Tidewave::Tools::GetLogs < Tidewave::Tool
6
+ DESCRIPTION = <<~DESCRIPTION.freeze
6
7
  Returns all log output, excluding logs that were caused by other tool calls.
7
8
 
8
9
  Use this tool to check for request logs or potentially logged errors.
9
10
  DESCRIPTION
10
11
 
11
- arguments do
12
- required(:tail).filled(:integer).description("The number of log entries to return from the end of the log")
13
- optional(:grep).filled(:string).description("Filter logs with the given regular expression (case insensitive). E.g. \"error\" when you want to capture errors in particular")
12
+ def initialize(options = {})
13
+ @log_file = options[:log_file] ? Pathname.new(options[:log_file].to_s) : nil
14
+ end
15
+
16
+ def definition
17
+ return nil unless @log_file
18
+
19
+ {
20
+ "name" => "get_logs",
21
+ "description" => DESCRIPTION,
22
+ "inputSchema" => {
23
+ "type" => "object",
24
+ "properties" => {
25
+ "tail" => {
26
+ "type" => "integer",
27
+ "not" => {
28
+ "type" => "null"
29
+ },
30
+ "description" => "The number of log entries to return from the end of the log"
31
+ },
32
+ "grep" => {
33
+ "type" => "string",
34
+ "minLength" => 1,
35
+ "description" => "Filter logs with the given regular expression (case insensitive). E.g. \"error\" when you want to capture errors in particular"
36
+ }
37
+ },
38
+ "required" => [ "tail" ]
39
+ }
40
+ }
14
41
  end
15
42
 
16
- def call(tail:, grep: nil)
17
- log_file = Rails.root.join("log", "#{Rails.env}.log")
18
- return "Log file not found" unless File.exist?(log_file)
43
+ def call(arguments)
44
+ tail = arguments.fetch("tail")
45
+ grep = arguments["grep"]
46
+ return "Log file not found" unless @log_file&.exist?
19
47
 
20
48
  regex = Regexp.new(grep, Regexp::IGNORECASE) if grep
21
49
  matching_lines = []
22
50
 
23
- tail_lines(log_file) do |line|
51
+ tail_lines(@log_file) do |line|
24
52
  if regex.nil? || line.match?(regex)
25
53
  matching_lines.unshift(line)
26
54
  break if matching_lines.size >= tail