tina4ruby 3.11.18 → 3.11.32

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: 2aa9ce02d7a7c924ae48d78b5ed1485261b85cde643ab13501040ea5bc7145da
4
- data.tar.gz: f97407bec1e490e48714ae6d00ee78ad211b74e50ec0f94325d92f3dbf935e43
3
+ metadata.gz: b1d2fabb4883ebcdd387f24cfc0c3e858304d7498b1439798b448c89623fa56e
4
+ data.tar.gz: 592c8eac568eba0950bde87ac4fd9b7d9bfed365aa23539b2f50409ff6a1f1d9
5
5
  SHA512:
6
- metadata.gz: c2bfb361e4de75738221dbdfb8f7a070cf224b71d02c389b0bc62171dd6566dea13d8fb0677f119ef331295d234fb1269d4a5a6573d026302d1c9d6c1b75a8e4
7
- data.tar.gz: ac914458d86220ae3788f9a7da1a3cd427d4b0901139b53f57e007f93103bb5d82ded233b449dcebad4ae8145624ca971e9d056d36ae103f808b66c5a67689b3
6
+ metadata.gz: '0917b8cbeaaa155cbf223bcb009662bed7f48e03e1b9b4d96c338a17fdf2cbac4333d88c549980d650a70eb515532b83783090cb4f3896ca8f7f53fba8307347'
7
+ data.tar.gz: c9b3da407eb7823e9e7b19af1d3928329d061b5a8d77bd904d96870ea89e90d9dbd0979b3909bff66011c584e556bb2c3dfcf219a078e6ffd28d2ac75cea9a7e
@@ -109,6 +109,15 @@ module Tina4
109
109
  @pool_size = pool # 0 = single connection, N>0 = N pooled connections
110
110
  @connected = false
111
111
 
112
+ # Per-instance thread-local key for the transaction adapter pin.
113
+ # Without this pin, every Database method call rotates to a different
114
+ # pooled connection. Inside a transaction this silently breaks atomicity:
115
+ # start_transaction begins on adapter A, executes autocommit on B/C, and
116
+ # commit/rollback land on D — a no-op. start_transaction sets the pin,
117
+ # commit/rollback clear it. While pinned, current_driver returns the same
118
+ # driver for every call so the whole transaction runs on one connection.
119
+ @tx_pin_key = :"tina4_pinned_adapter_#{object_id}"
120
+
112
121
  # Query cache — off by default, opt-in via TINA4_DB_CACHE=true
113
122
  @cache_enabled = truthy?(ENV["TINA4_DB_CACHE"])
114
123
  @cache_ttl = (ENV["TINA4_DB_CACHE_TTL"] || "30").to_i
@@ -161,7 +170,14 @@ module Tina4
161
170
  end
162
171
 
163
172
  # Get the current driver — from pool (round-robin) or single connection.
173
+ #
174
+ # Inside a transaction, all calls must land on the SAME driver — otherwise
175
+ # start_transaction, execute, and commit each rotate to a different pooled
176
+ # connection and the transaction is meaningless. start_transaction pins
177
+ # the driver to the calling thread; commit/rollback release it.
164
178
  def current_driver
179
+ pinned = Thread.current[@tx_pin_key]
180
+ return pinned if pinned
165
181
  if @pool
166
182
  @pool.checkout
167
183
  else
@@ -355,27 +371,38 @@ module Tina4
355
371
 
356
372
  def transaction
357
373
  drv = current_driver
374
+ Thread.current[@tx_pin_key] = drv
358
375
  drv.begin_transaction
359
376
  yield self
360
377
  drv.commit
361
378
  rescue => e
362
- drv.rollback
379
+ drv.rollback if drv
363
380
  raise e
381
+ ensure
382
+ Thread.current[@tx_pin_key] = nil
364
383
  end
365
384
 
366
385
  # Begin a transaction without a block — matches PHP/Python/Node API.
386
+ # Pins the driver to this thread for the whole transaction so executes
387
+ # and the final commit/rollback all run on the same connection.
367
388
  def start_transaction
368
- current_driver.begin_transaction
389
+ drv = current_driver
390
+ Thread.current[@tx_pin_key] = drv
391
+ drv.begin_transaction
369
392
  end
370
393
 
371
- # Commit the current transaction matches PHP/Python/Node API.
394
+ # Commit the current transaction and release the driver pin.
372
395
  def commit
373
396
  current_driver.commit
397
+ ensure
398
+ Thread.current[@tx_pin_key] = nil
374
399
  end
375
400
 
376
- # Roll back the current transaction matches PHP/Python/Node API.
401
+ # Roll back the current transaction and release the driver pin.
377
402
  def rollback
378
403
  current_driver.rollback
404
+ ensure
405
+ Thread.current[@tx_pin_key] = nil
379
406
  end
380
407
 
381
408
  def tables
@@ -316,9 +316,55 @@ module Tina4
316
316
  Tina4::Env.is_truthy(ENV["TINA4_DEBUG"])
317
317
  end
318
318
 
319
+ # Write `.tina4/mcp.json` so MCP-aware tools (Claude Code, Cursor) can
320
+ # auto-discover this project's live docs server. Idempotent — no-op if
321
+ # the file already matches the desired contents. Also appends `.tina4/`
322
+ # to the project's `.gitignore` if a git repo is present.
323
+ def auto_discover_mcp!(project_root: Dir.pwd)
324
+ return if @_mcp_auto_discovered
325
+ @_mcp_auto_discovered = true
326
+ return unless enabled?
327
+
328
+ port = ENV["TINA4_PORT"] || ENV["PORT"] || "7147"
329
+ url = "http://localhost:#{port}/__dev/api/mcp"
330
+ target_dir = File.join(project_root, ".tina4")
331
+ target = File.join(target_dir, "mcp.json")
332
+ payload = {
333
+ "mcpServers" => {
334
+ "tina4-live-docs" => {
335
+ "url" => url,
336
+ "description" => "Live API docs for this Tina4 project (framework + user code)",
337
+ },
338
+ },
339
+ }
340
+ begin
341
+ FileUtils.mkdir_p(target_dir)
342
+ existing = if File.file?(target)
343
+ (JSON.parse(File.read(target)) rescue {})
344
+ else
345
+ {}
346
+ end
347
+ if existing != payload
348
+ File.write(target, JSON.pretty_generate(payload))
349
+ end
350
+
351
+ gitignore = File.join(project_root, ".gitignore")
352
+ if File.directory?(File.join(project_root, ".git"))
353
+ current = File.file?(gitignore) ? File.read(gitignore) : ""
354
+ unless current.lines.map(&:strip).include?(".tina4/") || current.lines.map(&:strip).include?(".tina4")
355
+ prefix = (!current.empty? && !current.end_with?("\n")) ? "\n" : ""
356
+ File.write(gitignore, "#{current}#{prefix}.tina4/\n")
357
+ end
358
+ end
359
+ rescue StandardError => e
360
+ Tina4::Log.warning("auto_discover_mcp! failed: #{e.message}") if defined?(Tina4::Log)
361
+ end
362
+ end
363
+
319
364
  # Handle a /__dev request; returns [status, headers, body] or nil if not a dev path
320
365
  def handle_request(env)
321
366
  return nil unless enabled?
367
+ auto_discover_mcp!
322
368
 
323
369
  path = env["PATH_INFO"] || "/"
324
370
  method = env["REQUEST_METHOD"]
@@ -548,6 +594,16 @@ module Tina4
548
594
  when ["POST", "/__dev/api/scaffold/run"]
549
595
  body = read_json_body(env) || {}
550
596
  json_response(scaffold_run(body))
597
+ when ["GET", "/__dev/api/docs/search"]
598
+ json_response(docs_search_payload(env))
599
+ when ["GET", "/__dev/api/docs/class"]
600
+ json_response(docs_class_payload(query_param(env, "name")))
601
+ when ["GET", "/__dev/api/docs/method"]
602
+ json_response(docs_method_payload(query_param(env, "class"), query_param(env, "name")))
603
+ when ["GET", "/__dev/api/docs/index"]
604
+ json_response(docs_index_payload(query_param(env, "source")))
605
+ when ["GET", "/__dev/api/docs/.well-known.json"]
606
+ json_response(docs_well_known_payload)
551
607
  when ["GET", "/__dev/api/graphql/schema"]
552
608
  begin
553
609
  gql = Tina4::GraphQL.new
@@ -1286,6 +1342,56 @@ module Tina4
1286
1342
  { ok: false, error: "unknown kind: #{kind}" }
1287
1343
  end
1288
1344
  end
1345
+
1346
+ # ── Live Docs (Live API RAG) ─────────────────────────────────
1347
+
1348
+ def docs_search_payload(env)
1349
+ q = (query_param(env, "q") || "").to_s
1350
+ k = (query_param(env, "k") || "5").to_i
1351
+ source = (query_param(env, "source") || "all").to_s
1352
+ include_private = %w[1 true yes].include?((query_param(env, "include_private") || "").to_s.downcase)
1353
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1354
+ results = Tina4::Docs.cached(Dir.pwd).search(
1355
+ q, k: k, source: source, include_private: include_private
1356
+ )
1357
+ took_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000.0).round(2)
1358
+ { ok: true, query: q, results: results, took_ms: took_ms }
1359
+ end
1360
+
1361
+ def docs_class_payload(name)
1362
+ spec = Tina4::Docs.cached(Dir.pwd).class_spec(name.to_s)
1363
+ return { ok: false, error: "class not found: #{name}" } if spec.nil?
1364
+ { ok: true, class: spec }
1365
+ end
1366
+
1367
+ def docs_method_payload(class_fqn, name)
1368
+ spec = Tina4::Docs.cached(Dir.pwd).method_spec(class_fqn.to_s, name.to_s)
1369
+ return { ok: false, error: "method not found: #{class_fqn}##{name}" } if spec.nil?
1370
+ { ok: true, method: spec }
1371
+ end
1372
+
1373
+ def docs_index_payload(source)
1374
+ idx = Tina4::Docs.cached(Dir.pwd).index
1375
+ idx = idx.select { |e| e[:source] == source } if source && %w[framework user vendor].include?(source.to_s)
1376
+ { ok: true, count: idx.size, entries: idx }
1377
+ end
1378
+
1379
+ def docs_well_known_payload
1380
+ {
1381
+ ok: true,
1382
+ service: "tina4-live-docs",
1383
+ version: Tina4::VERSION,
1384
+ framework: "tina4-ruby",
1385
+ endpoints: {
1386
+ search: "/__dev/api/docs/search?q=<query>&k=<int>&source=<framework|user|all>&include_private=<bool>",
1387
+ class: "/__dev/api/docs/class?name=<fqn>",
1388
+ method: "/__dev/api/docs/method?class=<fqn>&name=<method>",
1389
+ index: "/__dev/api/docs/index?source=<framework|user|all>",
1390
+ },
1391
+ mcp: { url: "/__dev/api/mcp", tools: %w[api_search api_class api_method] },
1392
+ spec: "https://tina4.com — Live API RAG plan/v3/22-LIVE-API-RAG.md",
1393
+ }
1394
+ end
1289
1395
  end
1290
1396
  end
1291
1397
  end