tina4ruby 3.13.145 → 3.13.147

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: fdd61b88184de108dd6f4daefae312d7d26eab819ec084b88f5f257fce98167f
4
- data.tar.gz: 336322ba4f2b6521d74af052391a0c1464310330935dd507c6a4ab2bfb5add11
3
+ metadata.gz: e9a60268ad13cea010f5fc994563e37b77d62c93bf73a67d075cd8086b4fa431
4
+ data.tar.gz: 740402d29d856535fe82468ce329ea754760a180f24436b22ae36f1d16fc25b8
5
5
  SHA512:
6
- metadata.gz: e71cd5ffa73b75f87542f96e32fa4c503b38dacad2ea7444e381bd67ca94764be6a5c24f585e5cb1c4d4589b066303b02cef915effc1a8b8c92bd1b597176e6d
7
- data.tar.gz: d0389a5a87a2911f7130647e12ee7f7b9c8476e75f8dcd824659d9f72c0099b640687a2a778f385ea322afb66dd8d62e435cd84467b788a9616868c1aa62d75d
6
+ metadata.gz: 0a63f77fa42d0b2dfb1ce82a6c600c0b83b3336a68826302e27179dd92eb0bb79d57f0d4b5c183b279178d0d600fbac33a4f97ae0a1c415cae09cab3e2eaa2c5
7
+ data.tar.gz: 6bbd112086e7b380d4a9748ae96192fba1721b17bd7b57cff1a682e60c062c308b817e5053ee3d1fb7293f1a27e9f448857a526f1d4fd7fe72889308bbf56643
data/CHANGELOG.md CHANGED
@@ -6,6 +6,26 @@ 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.147 — 2026-10-05
10
+
11
+ ### Dev MCP tools (fixes #271)
12
+ - `database_columns` reads schema metadata so an empty table returns its columns; a missing table returns a clear `table not found` error.
13
+ - Tool argument validation returns an actionable `missing/unknown argument` error instead of a raw Ruby error; `api_method` now populates `params` and `return`.
14
+ - `route_list` entries include each route's `middleware`.
15
+ - A new route file added at runtime surfaces a signal.
16
+ - Developer skill corrected: per-language hot-reload reality, and `api_method` shown with argument names.
17
+
18
+ ## 3.13.146 — 2026-10-04
19
+
20
+ ### AI skills
21
+ - Every skill now opens with a content list (table of contents) and a degrees-of-freedom legend (what is inviolable, what is a default, what is judgement).
22
+ - New `tina4-cli` skill documents every `tina4` command and flag and the scaffolding discipline.
23
+ - `tina4-maintainer` gains task checklists (PR review, release, installer signing, parity sweep); `tina4-design` split into focused reference files.
24
+ - Skills installer now supports one-level-nested references; `install-skills.ps1` re-signed.
25
+
26
+ ### Framework
27
+ - No framework code changes - the version bump carries the skills release across all four frameworks at parity.
28
+
9
29
  ## 3.13.145 — 2026-10-03
10
30
 
11
31
  Two fixes land together. The Api client no longer carries an application-configured header onto a different origin: credentials travel under many names (`X-Api-Key`, a custom bearer header), so on a cross-origin redirect only content-negotiation and transport headers cross, and every configured or per-call header is bound to the origin it was meant for; a same-origin redirect keeps them. And `tina4ruby migrate` now exits 1 when there is no usable database (unreachable or misconfigured), so CI no longer reads a failed migration as success. Both are proven against real dependencies, no mocks. No runtime gems.
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/version.rb CHANGED
@@ -7,5 +7,5 @@
7
7
 
8
8
 
9
9
  module Tina4
10
- VERSION = "3.13.145"
10
+ VERSION = "3.13.147"
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.145
4
+ version: 3.13.147
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-03 00:00:00.000000000 Z
11
+ date: 2026-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3