tina4ruby 3.13.146 → 3.13.148

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: aaafd9078b79dd56e7829b0a92ee162c3eaf471c7102746ed1b119f523f42b8e
4
- data.tar.gz: eaa7f47dd692a137d5fce2395696a3f84addfb1feaf8a74b96f936f4177ffcf4
3
+ metadata.gz: 5f81aa317ab408c5f85fa182149d2bddf74f76a6dbe0d07eab1346335f2feb18
4
+ data.tar.gz: a877a9d71c42570d719c721427d164581919c15e235e17d366891a5337a7920c
5
5
  SHA512:
6
- metadata.gz: 2b50b1109f4b5caf4001fde5141a248c5eaf7d6b9fb691eebe267235f2b7ad2f513b771eefa7233b4b6ef849908aad2e5671b04adee72cdbb674702ba9b2f1a1
7
- data.tar.gz: dbc2ffdc2dd55b7ca03b8de790fc24793edf4ff58de61eeec66196444c8bd8788596cc9c518cecf83c1e8021e8710592f3a947e1039a15da2c5f8305c3e955ea
6
+ metadata.gz: 4da03de83c10544ac690ce24d6b951b6ba688fab57fb01cb4cf5a630d1e1d41dd5fbab7397538a2910708409c3fb6fea74285ce84fd74559fb883d83303b66d6
7
+ data.tar.gz: cb21b614d729b774a1d9ec16ada8674cab6663e675a370ed7cb8b3f588b3c038036ba0d43bedcf764f1ab673b117df005e30063d50612ce8ace479300104d127
data/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@ number means the same thing everywhere.
6
6
  **The authoritative release notes for every shipped version live in the documentation:**
7
7
  https://tina4.com/ruby/36-releases
8
8
 
9
+ ## 3.13.148 — 2026-10-05
10
+
11
+ ### Session
12
+ - An anonymous request stores no session — a request that never reads or writes session state no longer emits a `Set-Cookie` or persists an empty session record (cross-framework fix, at parity across Python, PHP, Ruby, Node.js).
13
+
14
+ ### AI skills
15
+ - `tina4-developer-ruby` skill `updated_for_version` bumped to this release.
16
+
17
+ ## 3.13.147 — 2026-10-05
18
+
19
+ ### Dev MCP tools (fixes #271)
20
+ - `database_columns` reads schema metadata so an empty table returns its columns; a missing table returns a clear `table not found` error.
21
+ - Tool argument validation returns an actionable `missing/unknown argument` error instead of a raw Ruby error; `api_method` now populates `params` and `return`.
22
+ - `route_list` entries include each route's `middleware`.
23
+ - A new route file added at runtime surfaces a signal.
24
+ - Developer skill corrected: per-language hot-reload reality, and `api_method` shown with argument names.
25
+
9
26
  ## 3.13.146 — 2026-10-04
10
27
 
11
28
  ### AI skills
@@ -649,12 +649,11 @@ module Tina4
649
649
  # renamed through TINA4_SESSION_NAME, so the auto-Set-Cookie would be
650
650
  # needlessly re-emitted on every request that already carries the renamed
651
651
  # cookie. Parity with Python's core/server._init_session cookie_prefix.
652
- sid = sess.id
653
652
  cookie_prefix = "#{Tina4::Session.cookie_name}="
654
653
  cookie_val = (ctx.env["HTTP_COOKIE"] || "").split(";").map(&:strip)
655
654
  .find { |part| part.start_with?(cookie_prefix) }
656
655
  &.slice(cookie_prefix.length..)
657
- if sid && sid != cookie_val
656
+ if session_needs_cookie?(sess, cookie_val)
658
657
  # Route through Session#cookie_header rather than hand-writing the
659
658
  # header, so TINA4_SESSION_SECURE / _SAMESITE / _HTTPONLY / _NAME / _TTL
660
659
  # are all honoured and Secure reflects the request scheme. The old
@@ -666,6 +665,19 @@ module Tina4
666
665
  [status, headers, body_parts]
667
666
  end
668
667
 
668
+ # Whether the response must carry a session cookie: the session has an id the
669
+ # request did not already send, and it is not a fresh one. A fresh session
670
+ # (minted for this request and never written: a route that only read it, or
671
+ # the replacement for a cookie the store does not know) was not stored, so a
672
+ # cookie for it would name nothing - the next request could not resume it and
673
+ # would be handed another one.
674
+ def session_needs_cookie?(sess, cookie_val)
675
+ sid = sess.id
676
+ return false unless sid && sid != cookie_val
677
+
678
+ !(sess.respond_to?(:fresh?) && sess.fresh?)
679
+ end
680
+
669
681
  # Milliseconds since the request entered the pipeline.
670
682
  def elapsed_ms(ctx)
671
683
  ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - ctx.started_at) * 1000).round(3)
data/lib/tina4/docs.rb CHANGED
@@ -432,6 +432,7 @@ module Tina4
432
432
  private_flag = visibility != "public" || underscored
433
433
 
434
434
  summary, doc = split_doc(pending_doc)
435
+ returns = pending_doc.join(" ")[/@return\s+\[([^\]]+)\]/, 1].to_s
435
436
  method_fqn = if is_static
436
437
  "#{current_class[:fqn]}.#{mname}"
437
438
  else
@@ -446,6 +447,7 @@ module Tina4
446
447
  class_fqn: current_class[:fqn],
447
448
  class: current_class[:fqn],
448
449
  signature: signature,
450
+ returns: returns,
449
451
  summary: summary,
450
452
  docstring: doc,
451
453
  file: rel_file,
@@ -620,11 +622,54 @@ module Tina4
620
622
  static: entry[:static] || false,
621
623
  source: entry[:source],
622
624
  version: entry[:version],
623
- params: [],
624
- return: "",
625
+ params: params_from_signature(entry[:signature]),
626
+ return: entry[:returns].to_s,
625
627
  }
626
628
  end
627
629
 
630
+ # Structured params parsed from a Ruby signature such as
631
+ # "fetch(sql, params = [], limit: 100, **opts)" -- same shape Python
632
+ # reports: [{name, type, required, default}]. Ruby has no inline type
633
+ # annotations, so type is "" unless the docblock says otherwise.
634
+ def params_from_signature(signature)
635
+ inner = signature.to_s[/\A[^(]*\((.*)\)\z/m, 1]
636
+ return [] if inner.nil? || inner.strip.empty?
637
+
638
+ split_top_level_commas(inner).map { |piece| param_entry(piece.strip) }
639
+ end
640
+
641
+ def param_entry(piece)
642
+ prefix = piece[/\A(\*\*|\*|&)/, 1]
643
+ return { name: piece, type: "", required: false, default: nil } if prefix
644
+
645
+ keyword = piece.match(/\A([A-Za-z_]\w*):\s*(.*)\z/m)
646
+ if keyword
647
+ default = keyword[2].strip
648
+ return { name: keyword[1], type: "", required: default.empty?, default: default.empty? ? nil : default }
649
+ end
650
+
651
+ name, default = piece.split(/\s*=\s*/, 2)
652
+ { name: name.strip, type: "", required: default.nil?, default: default&.strip }
653
+ end
654
+
655
+ def split_top_level_commas(text)
656
+ pieces = []
657
+ depth = 0
658
+ current = +""
659
+ text.each_char do |ch|
660
+ depth += 1 if "([{".include?(ch)
661
+ depth -= 1 if ")]}".include?(ch)
662
+ if ch == "," && depth.zero?
663
+ pieces << current
664
+ current = +""
665
+ else
666
+ current << ch
667
+ end
668
+ end
669
+ pieces << current unless current.strip.empty?
670
+ pieces
671
+ end
672
+
628
673
  # ── Sync writer ──────────────────────────────────────────────────
629
674
 
630
675
  def render_generated_block
data/lib/tina4/mcp.rb CHANGED
@@ -139,6 +139,17 @@ module Tina4
139
139
  schema
140
140
  end
141
141
 
142
+ # Readable name for one attached route middleware (class, instance, string
143
+ # spec or lambda) -- what route_list reports under "middleware".
144
+ def self.middleware_label(attached)
145
+ case attached
146
+ when ::String then attached
147
+ when ::Module then attached.name || attached.inspect
148
+ when ::Proc, ::Method then "proc"
149
+ else attached.class.name || attached.inspect
150
+ end
151
+ end
152
+
142
153
  # Informational only — whether the CONFIGURED host looks local.
143
154
  #
144
155
  # NOT the security gate. This reads TINA4_HOST_NAME (the configured bind
@@ -510,22 +521,47 @@ module Tina4
510
521
  raise ArgumentError, "Unknown tool: #{tool_name}" if tool.nil?
511
522
 
512
523
  arguments = params.fetch("arguments", {})
524
+ arguments = {} if arguments.nil?
513
525
  handler = tool["handler"]
514
526
 
527
+ # Validate against the tool's input schema BEFORE invoking, so a missing
528
+ # or unknown argument is an actionable message, never a raw Ruby
529
+ # ArgumentError. Python parity (tina4_python.mcp).
530
+ argument_error = validate_tool_arguments(tool_name, tool["inputSchema"], arguments)
531
+ return mcp_text_content(argument_error) if argument_error
532
+
515
533
  # Call the handler -- support both keyword and positional args
516
534
  result = _invoke_handler(handler, arguments)
517
535
 
518
- # Format result as MCP content
519
- content = case result
520
- when String
521
- [{ "type" => "text", "text" => result }]
522
- when Hash, Array
523
- [{ "type" => "text", "text" => JSON.pretty_generate(result) }]
524
- else
525
- [{ "type" => "text", "text" => result.to_s }]
526
- end
536
+ mcp_text_content(result)
537
+ end
538
+
539
+ # Format a tool result as MCP content.
540
+ def mcp_text_content(result)
541
+ text = case result
542
+ when String then result
543
+ when Hash, Array then JSON.pretty_generate(result)
544
+ else result.to_s
545
+ end
546
+ { "content" => [{ "type" => "text", "text" => text }] }
547
+ end
548
+
549
+ # Returns nil when the arguments satisfy the schema, else an
550
+ # { "error" => "missing required argument 'p' (tool takes p1, p2)" } hash
551
+ # (or "unknown argument ..." with the same shape).
552
+ def validate_tool_arguments(tool_name, schema, arguments)
553
+ return { "error" => "arguments for #{tool_name} must be an object" } unless arguments.is_a?(Hash)
527
554
 
528
- { "content" => content }
555
+ properties = (schema || {}).fetch("properties", {}).keys
556
+ takes = "(#{tool_name} takes #{properties.join(', ')})"
557
+
558
+ missing = (schema || {}).fetch("required", []).find { |name| !arguments.key?(name) }
559
+ return { "error" => "missing required argument '#{missing}' #{takes}" } if missing
560
+
561
+ unknown = arguments.keys.map(&:to_s).find { |name| !properties.include?(name) }
562
+ return { "error" => "unknown argument '#{unknown}' #{takes}" } if unknown
563
+
564
+ nil
529
565
  end
530
566
 
531
567
  def _handle_resources_list(_params)
@@ -888,7 +924,13 @@ module Tina4
888
924
  unless table.to_s =~ /\A[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?\z/
889
925
  return { "error" => "Invalid table name" }
890
926
  end
891
- db.columns(table)
927
+ # Schema metadata (PRAGMA / information_schema), never the first row, so
928
+ # an EMPTY table still reports its columns. A table that does not exist
929
+ # has no columns: say so rather than answering [].
930
+ columns = db.columns(table)
931
+ next { "error" => "table not found: #{table}" } if columns.nil? || columns.empty?
932
+
933
+ columns
892
934
  }, "Get column definitions for a table")
893
935
 
894
936
  # ── Route Tools ───────────────────────────────────
@@ -899,7 +941,8 @@ module Tina4
899
941
  {
900
942
  "method" => route.method.to_s,
901
943
  "path" => route.path.to_s,
902
- "auth_required" => route.auth_required ? true : false
944
+ "auth_required" => route.auth_required ? true : false,
945
+ "middleware" => route.middleware.map { |attached| Tina4.middleware_label(attached) }
903
946
  }
904
947
  end
905
948
  }, "List all registered routes")
@@ -1458,9 +1501,14 @@ module Tina4
1458
1501
  Tina4::Docs.cached(project_root).class_spec(name.to_s)
1459
1502
  }, "Full class reflection (methods, file, line) from the live API index")
1460
1503
 
1461
- server.register_tool("api_method", lambda { |class_name:, name:|
1462
- Tina4::Docs.cached(project_root).method_spec(class_name.to_s, name.to_s)
1463
- }, "Single method spec (signature, summary, file, line) from the live API index")
1504
+ server.register_tool("api_method", lambda { |name:, **rest|
1505
+ class_name = rest[:class].to_s
1506
+ Tina4::Docs.cached(project_root).method_spec(class_name, name.to_s) ||
1507
+ { "error" => "method not found: #{class_name}.#{name}" }
1508
+ }, "Single method spec (signature, params, return, file, line) from the live API index",
1509
+ { "type" => "object",
1510
+ "properties" => { "class" => { "type" => "string" }, "name" => { "type" => "string" } },
1511
+ "required" => %w[class name] })
1464
1512
 
1465
1513
  # ── Code/doc grounding (semantic FTS over this repo's source) ──
1466
1514
  # code_search is the fuzzy DUAL of the structural api_* tools:
data/lib/tina4/session.rb CHANGED
@@ -217,7 +217,7 @@ module Tina4
217
217
  # false) therefore still no-ops, exactly as before; a modified session of
218
218
  # any kind still writes.
219
219
  def save
220
- return true unless @id && (@modified || @stored)
220
+ return true if @id.nil? || fresh?
221
221
 
222
222
  record = @data
223
223
  if @stored
@@ -247,6 +247,17 @@ module Tina4
247
247
  end
248
248
  end
249
249
 
250
+ # Whether this session is only an id: nothing has stored it and it holds no
251
+ # data. Such a session has no record and needs no cookie, so #save writes
252
+ # nothing for it and the dispatcher sends no cookie for it. It stops being
253
+ # fresh when something is stored in it. Calls that mark a session changed
254
+ # without leaving anything in it (deleting a key it never had, #clear,
255
+ # #regenerate on an empty session) leave it fresh: a record with no data is
256
+ # not a session.
257
+ def fresh?
258
+ !@stored && @data.empty?
259
+ end
260
+
250
261
  # Destroy the current session. ENDS the session: the stored record is
251
262
  # removed and the id is CLEARED (@id = nil), so a later set()+save() with no
252
263
  # new #start has no id to persist under and cannot RESURRECT the just-
data/lib/tina4/version.rb CHANGED
@@ -7,5 +7,5 @@
7
7
 
8
8
 
9
9
  module Tina4
10
- VERSION = "3.13.146"
10
+ VERSION = "3.13.148"
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.13.146
4
+ version: 3.13.148
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-04 00:00:00.000000000 Z
11
+ date: 2026-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3