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 +4 -4
- data/lib/tina4/database.rb +31 -4
- data/lib/tina4/dev_admin.rb +106 -0
- data/lib/tina4/docs.rb +636 -0
- data/lib/tina4/mcp.rb +15 -0
- data/lib/tina4/public/js/tina4-dev-admin.js +121 -121
- data/lib/tina4/public/js/tina4-dev-admin.min.js +121 -121
- data/lib/tina4/rack_app.rb +46 -1
- data/lib/tina4/response.rb +3 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b1d2fabb4883ebcdd387f24cfc0c3e858304d7498b1439798b448c89623fa56e
|
|
4
|
+
data.tar.gz: 592c8eac568eba0950bde87ac4fd9b7d9bfed365aa23539b2f50409ff6a1f1d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0917b8cbeaaa155cbf223bcb009662bed7f48e03e1b9b4d96c338a17fdf2cbac4333d88c549980d650a70eb515532b83783090cb4f3896ca8f7f53fba8307347'
|
|
7
|
+
data.tar.gz: c9b3da407eb7823e9e7b19af1d3928329d061b5a8d77bd904d96870ea89e90d9dbd0979b3909bff66011c584e556bb2c3dfcf219a078e6ffd28d2ac75cea9a7e
|
data/lib/tina4/database.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
data/lib/tina4/dev_admin.rb
CHANGED
|
@@ -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
|