tina4ruby 3.13.132 → 3.13.133

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: bf58fba1e80ebbfcdeccfdf5ecab50bf0ce08754d834eaffe29e9e7e37ff4363
4
- data.tar.gz: f59a4a709438114acd05275ee4220e032ebe539b3d27547b377de492f2583038
3
+ metadata.gz: 83f372e751a027c81f97a04d5002b5dd756f16d9f94bc9a9b771c820f0878619
4
+ data.tar.gz: 6436d18bc08e5dd6f5b6232feb90a7e2650a728064420f12e505eb279e3b941c
5
5
  SHA512:
6
- metadata.gz: 3b4c3b693f8686a0119ed679627ab646439c4e7e4b75649e55e70e5207556582b06bd3413fe4560cbc60183f6f66f6732f4fe11e4c87b7358b2b7327976f5841
7
- data.tar.gz: 93b12a0719367dc9c8e4f1a95339d43ae170ea14e4ea52c347d9419044ad508ff894c08ba11a1c930920365a779ca5e0ab95a7e442a8fe60383d35d5fdeac045
6
+ metadata.gz: 8c214ae801254e82779984e62aabf4c07895c45109e25a75a28ea48aecb41aef707edfdbaed6a86db94e458b3bfdf06df783fc1e398bd7e77a1bc7796cc967b8
7
+ data.tar.gz: e98653bdc54a68d5c9c205645651b0b682ce1d23912d5dc437d774161636d09abfe1225fc01366ddcc2f0ad38f69fe2bd9984dc77579541f5876bae41e973c4b
data/CHANGELOG.md CHANGED
@@ -6,6 +6,96 @@ 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.133
10
+
11
+ Maintainability pass, no behavior changes. Swagger.generate decomposed into cohesive
12
+ helpers (Python 64->6, PHP 83->6, Node 52->2 cyclomatic; Ruby already lean) with the
13
+ OpenAPI document byte-for-byte identical. Ruby dev-admin handle_request split from a
14
+ 138-branch dispatcher into per-concern methods (CC 138->11); routes, responses, and
15
+ the localhost/auth gate unchanged. Full notes: https://tina4.com/python/36-releases
16
+
17
+ ## 3.13.132
18
+
19
+ ORM reads return a ModelCollection carrying the query total (ADR-0064): where,
20
+ select, find (filter form), all, and with_trashed hand back the page of models AND
21
+ the total for the filter, so you paginate without a second query. get_total_records()
22
+ / to_paginate() (Python, Ruby); getTotalRecords() / toPaginate() (PHP, Node). The
23
+ total reuses the count the query already ran - zero extra queries. Python's
24
+ where(..., with_count=True) tuple is removed; PHP's return type changes from a bare
25
+ array to an array-compatible ModelCollection object (toArray() escape hatch). Also:
26
+ the dev server binds dual-stack loopback so localhost works on Windows; the dev-admin
27
+ agent chat surface is removed and the dashboard no longer self-reloads; Firebird DDL
28
+ no longer reads a row count and Firebird adapters close cleanly on GC (Python); the
29
+ Valkey session handler subclasses Redis and shared fetch/execute/Frond loops are
30
+ hoisted (behaviour unchanged). Full notes: https://tina4.com/python/36-releases
31
+
32
+ ## 3.13.131
33
+
34
+ AI skills for a global team: plain English, short replies, effort matched to the
35
+ task, ask-before-guess, and a grounding fallback for tools without the coder server.
36
+
37
+ ## 3.13.130
38
+
39
+ AI skills discipline pass: objective on ideas (disagree when a design is weak),
40
+ claim only what was verified, and chat instead of narrate.
41
+
42
+ ## 3.13.129
43
+
44
+ tina4 generate model stops silently pluralising a reserved-word table. It prints a
45
+ note and offers --table-name, so a downstream route or query does not fail with no
46
+ clue the table was renamed.
47
+
48
+ ## 3.13.128
49
+
50
+ tina4 lint: one command runs the project linter (ruff, phpcs, rubocop, or eslint),
51
+ installs it as a dev dependency on demand, and falls back to a zero-dependency syntax
52
+ baseline with --no-install.
53
+
54
+ ## 3.13.127
55
+
56
+ A bare TRUE or FALSE now reaches SQL Server and Firebird as 1/0 in every framework.
57
+ Node, PHP and Ruby had skipped the boolean translation on SQL Server, and Ruby on
58
+ Firebird too.
59
+
60
+ ## 3.13.126
61
+
62
+ The fake-data seeder reads the column name: a products.name column fills with product
63
+ names, not person names.
64
+
65
+ ## 3.13.125
66
+
67
+ Scaffolded migrations apply on every engine. tina4 generate migration wrote
68
+ SQLite-only DDL (TEXT, REAL, CREATE TABLE IF NOT EXISTS); the generators now emit
69
+ engine-correct types so a migration runs on Firebird and the rest.
70
+
71
+ ## 3.13.124
72
+
73
+ The database session backend works on Firebird in all four frameworks, with a
74
+ session-engine test in each so the gap cannot reopen.
75
+
76
+ ## 3.13.123
77
+
78
+ tina4 generate queue <topic> returns the generate_v1_1 envelope: edit_hints[] naming
79
+ the handler line to fill and next[] naming what to run after.
80
+
81
+ ## 3.13.122
82
+
83
+ The default Content-Security-Policy (default-src 'self') no longer fails silently:
84
+ when an app relies on something the policy blocks, the framework surfaces it instead
85
+ of leaving a blank page.
86
+
87
+ ## 3.13.121
88
+
89
+ Every migration surface speaks the ADR-0063 generate_v1_1 envelope - the CLI
90
+ migrate:create, the MCP migration_create, and the rest, not just tina4 generate
91
+ migration. Adds a pre-tag release guard.
92
+
93
+ ## 3.13.120
94
+
95
+ Scaffolding envelope v1.1 (ADR-0063): every tina4 generate verb reports what to edit
96
+ next and what to do after, not just where files went. Introduces the tina4-architect
97
+ skill.
98
+
9
99
  ## 3.13.119
10
100
 
11
101
  Skill repair by @MichaelC8E (#44, merged as c1b8c43d5) plus a
@@ -395,6 +395,34 @@ module Tina4
395
395
  denial = dev_mutation_denial(env)
396
396
  return denial if denial
397
397
 
398
+ # Exact [method, path] dispatch, split by concern. Every route is an
399
+ # exact pair (no overlaps), so the groups form a partition: the first
400
+ # group that matches answers, and a group returns nil when it holds no
401
+ # matching route. Behaviour is identical to the former single 138-branch
402
+ # case — the only handler that legitimately returns nil is an MCP route
403
+ # with the capability disabled (with_mcp_gate), for which the original
404
+ # case also fell through to nil (RackApp then 404s it).
405
+ dispatch_dashboard(method, path, env) ||
406
+ dispatch_inspectors(method, path, env) ||
407
+ dispatch_mailbox(method, path, env) ||
408
+ dispatch_queue(method, path, env) ||
409
+ dispatch_data(method, path, env) ||
410
+ dispatch_config(method, path, env) ||
411
+ dispatch_files(method, path, env) ||
412
+ dispatch_mcp(method, path, env)
413
+ end
414
+
415
+
416
+ private
417
+
418
+ # ── /__dev request dispatch (split from a single 138-CC case) ──────
419
+ # Each dispatch_* owns one concern's exact [method, path] routes and
420
+ # returns a Rack triple, or nil when it holds no matching route. Branch
421
+ # bodies are the former case's, moved verbatim.
422
+
423
+ # Dashboard shell, injected-toolbar assets, the reload trigger and the
424
+ # core status / routes / system panels.
425
+ def dispatch_dashboard(method, path, env)
398
426
  case [method, path]
399
427
  when ["GET", "/__dev"], ["GET", "/__dev/"]
400
428
  serve_dashboard
@@ -413,54 +441,24 @@ module Tina4
413
441
  when ["GET", "/__dev/api/mtime"]
414
442
  json_response({ mtime: @reload_mtime || 0, file: @reload_file || "" })
415
443
  when ["POST", "/__dev/api/reload"]
416
- body = read_json_body(env) || {}
417
- @reload_mtime = Time.now.to_i
418
- @reload_file = body["file"] || ""
419
- reload_type = body["type"] || "reload"
420
- Tina4::Log.info("External reload trigger: #{reload_type}#{@reload_file.empty? ? '' : " (#{@reload_file})"}")
421
- # Re-discover so files dropped into src/routes/ register without
422
- # a server restart. Idempotent — already-loaded files are skipped,
423
- # changed files are re-loaded (mtime-tracked).
424
- begin
425
- Tina4::Router.rescan_routes!
426
- rescue StandardError => e
427
- Tina4::Log.error("Re-discover on reload failed: #{e.message}")
428
- end
429
- # Keep the code Context index LIVE on the same reload trigger: reindex
430
- # just the changed file (UPSERT) so the dev-MCP code_search reflects
431
- # the edit immediately. Only touches an already-built index
432
- # (existing_context never creates one); guarded so a context failure
433
- # never breaks the reload.
434
- begin
435
- unless @reload_file.to_s.empty?
436
- ctx = Tina4::Context.existing_context
437
- ctx&.reindex_file(@reload_file)
438
- end
439
- rescue StandardError => e
440
- Tina4::Log.error("Context reindex on reload failed: #{e.message}")
441
- end
442
- # WebSocket-primary reload: push an instant message to every browser
443
- # connected on /__dev_reload. The toolbar client (and the dev-admin
444
- # dashboard) act on this immediately — the mtime poll above is only a
445
- # fallback for when the socket is down. CSS changes swap stylesheets;
446
- # everything else triggers a full page reload. We normalise the wire
447
- # `type` to "css"/"reload" (the clients only react to css/reload/change)
448
- # but still echo the caller's original type in the HTTP response.
449
- # Wrapped so a broadcast failure — or zero connected clients — never
450
- # 500s the reload endpoint.
451
- begin
452
- ws_type = reload_type == "css" ? "css" : "reload"
453
- Tina4::DevReload.broadcast(
454
- JSON.generate({ type: ws_type, file: @reload_file, mtime: @reload_mtime })
455
- )
456
- rescue StandardError => e
457
- Tina4::Log.error("Dev-reload WebSocket broadcast failed: #{e.message}")
458
- end
459
- json_response({ ok: true, type: reload_type })
444
+ handle_reload(env)
460
445
  when ["GET", "/__dev/api/status"]
461
446
  json_response(status_payload)
462
447
  when ["GET", "/__dev/api/routes"]
463
448
  json_response(routes_payload)
449
+ when ["GET", "/__dev/api/system"]
450
+ json_response(system_payload)
451
+ when ["GET", "/__dev/api/version-check"]
452
+ json_response(version_check_payload)
453
+ when ["GET", "/__dev/api/git/status"]
454
+ json_response(git_status_payload)
455
+ end
456
+ end
457
+
458
+ # Observability panels: message log, request inspector and live
459
+ # WebSocket connections. (Mailbox + error tracker live in dispatch_mailbox.)
460
+ def dispatch_inspectors(method, path, env)
461
+ case [method, path]
464
462
  when ["GET", "/__dev/api/messages"]
465
463
  category = query_param(env, "category")
466
464
  messages = message_log.get(category: category)
@@ -471,68 +469,31 @@ module Tina4
471
469
  category = body["category"] if body
472
470
  message_log.clear(category: category)
473
471
  json_response({ cleared: true })
472
+ when ["GET", "/__dev/api/messages/search"]
473
+ keyword = query_param(env, "q") || query_param(env, "keyword") || ""
474
+ all_messages = message_log.get
475
+ filtered = keyword.empty? ? all_messages : all_messages.select { |m| m[:message].to_s.downcase.include?(keyword.downcase) }
476
+ json_response({ messages: filtered, count: filtered.size, keyword: keyword })
474
477
  when ["GET", "/__dev/api/requests"]
475
478
  limit = (query_param(env, "limit") || 50).to_i
476
479
  json_response({ requests: request_inspector.get(limit: limit), stats: request_inspector.stats })
477
480
  when ["POST", "/__dev/api/requests/clear"]
478
481
  request_inspector.clear
479
482
  json_response({ cleared: true })
480
- when ["GET", "/__dev/api/system"]
481
- json_response(system_payload)
482
- when ["GET", "/__dev/api/queue/topics"]
483
- json_response({ topics: queue_topics })
484
- when ["GET", "/__dev/api/queue/dead-letters"]
485
- topic = query_param(env, "topic") || "default"
486
- jobs = begin
487
- queue_dead_letters(topic)
488
- rescue StandardError
489
- []
490
- end
491
- json_response({ jobs: jobs, count: jobs.size, topic: topic })
492
- when ["GET", "/__dev/api/queue"]
493
- topic = query_param(env, "topic") || "default"
494
- stats = { pending: 0, completed: 0, failed: 0, reserved: 0 }
495
- jobs = []
496
- begin
497
- if defined?(Tina4::Queue)
498
- queue = Tina4::Queue.new(backend: :file, topic: topic)
499
- # Queue#size is keyword-only (def size(status:)). Calling it
500
- # positionally raises ArgumentError, which the rescue below
501
- # swallows — silently zeroing every stat. Use keyword form.
502
- stats = {
503
- pending: queue.size(status: "pending"),
504
- completed: queue.size(status: "completed"),
505
- failed: queue.size(status: "failed"),
506
- reserved: queue.size(status: "reserved"),
507
- }
508
- jobs = queue_jobs(topic, query_param(env, "status"))
509
- end
510
- rescue StandardError
511
- # fall through to empty stats
512
- end
513
- json_response({ jobs: jobs, stats: stats })
514
- when ["GET", "/__dev/api/mailbox"]
515
- messages = mailbox.inbox
516
- json_response({ messages: messages, count: messages.size, unread: mailbox.unread_count })
517
- when ["GET", "/__dev/api/broken"]
518
- errors = error_tracker.get(include_resolved: true)
519
- h = error_tracker.health
520
- json_response({ errors: errors, count: errors.size, health: h })
521
- when ["POST", "/__dev/api/broken/resolve"]
522
- body = read_json_body(env)
523
- id = body && body["id"]
524
- resolved = id ? error_tracker.resolve(id) : false
525
- json_response({ resolved: resolved, id: id })
526
- when ["POST", "/__dev/api/broken/clear"]
527
- # "Clear All" button — flush every tracked error, not only the
528
- # ones individually marked resolved. Matches PHP/Python.
529
- error_tracker.clear_all
530
- json_response({ cleared: true })
531
483
  when ["GET", "/__dev/api/websockets"]
532
484
  json_response(websockets_payload)
533
485
  when ["POST", "/__dev/api/websockets/disconnect"]
534
486
  body = read_json_body(env) || {}
535
487
  json_response(websockets_disconnect(body))
488
+ end
489
+ end
490
+
491
+ # Dev mailbox (email capture) and the error tracker panel.
492
+ def dispatch_mailbox(method, path, env)
493
+ case [method, path]
494
+ when ["GET", "/__dev/api/mailbox"]
495
+ messages = mailbox.inbox
496
+ json_response({ messages: messages, count: messages.size, unread: mailbox.unread_count })
536
497
  when ["GET", "/__dev/api/mailbox/read"]
537
498
  message_id = query_param(env, "id")
538
499
  message = mailbox.read(message_id)
@@ -550,11 +511,39 @@ module Tina4
550
511
  when ["POST", "/__dev/api/mailbox/clear"]
551
512
  mailbox.clear
552
513
  json_response({ cleared: true })
553
- when ["GET", "/__dev/api/messages/search"]
554
- keyword = query_param(env, "q") || query_param(env, "keyword") || ""
555
- all_messages = message_log.get
556
- filtered = keyword.empty? ? all_messages : all_messages.select { |m| m[:message].to_s.downcase.include?(keyword.downcase) }
557
- json_response({ messages: filtered, count: filtered.size, keyword: keyword })
514
+ when ["GET", "/__dev/api/broken"]
515
+ errors = error_tracker.get(include_resolved: true)
516
+ h = error_tracker.health
517
+ json_response({ errors: errors, count: errors.size, health: h })
518
+ when ["POST", "/__dev/api/broken/resolve"]
519
+ body = read_json_body(env)
520
+ id = body && body["id"]
521
+ resolved = id ? error_tracker.resolve(id) : false
522
+ json_response({ resolved: resolved, id: id })
523
+ when ["POST", "/__dev/api/broken/clear"]
524
+ # "Clear All" button — flush every tracked error, not only the
525
+ # ones individually marked resolved. Matches PHP/Python.
526
+ error_tracker.clear_all
527
+ json_response({ cleared: true })
528
+ end
529
+ end
530
+
531
+ # Queue panel: topic list, dead letters, per-topic overview and the
532
+ # retry / purge / replay mutations.
533
+ def dispatch_queue(method, path, env)
534
+ case [method, path]
535
+ when ["GET", "/__dev/api/queue/topics"]
536
+ json_response({ topics: queue_topics })
537
+ when ["GET", "/__dev/api/queue/dead-letters"]
538
+ topic = query_param(env, "topic") || "default"
539
+ jobs = begin
540
+ queue_dead_letters(topic)
541
+ rescue StandardError
542
+ []
543
+ end
544
+ json_response({ jobs: jobs, count: jobs.size, topic: topic })
545
+ when ["GET", "/__dev/api/queue"]
546
+ queue_overview(env)
558
547
  when ["POST", "/__dev/api/queue/retry"]
559
548
  body = read_json_body(env) || {}
560
549
  json_response(queue_retry(body))
@@ -564,18 +553,18 @@ module Tina4
564
553
  when ["POST", "/__dev/api/queue/replay"]
565
554
  body = read_json_body(env) || {}
566
555
  json_response(queue_replay(body))
556
+ end
557
+ end
558
+
559
+ # Data + tooling: table browser, seeding, the run-chips (migrate / test /
560
+ # seed), the SQL console, table list, the gallery and the metrics panels.
561
+ def dispatch_data(method, path, env)
562
+ case [method, path]
567
563
  when ["GET", "/__dev/api/table"]
568
564
  table_name = query_param(env, "name")
569
565
  json_response(table_detail_payload(table_name))
570
566
  when ["POST", "/__dev/api/seed"]
571
- body = read_json_body(env)
572
- table_name = (body && body["table"]) || ""
573
- count = (body && body["count"]) || 10
574
- seed = body && body["seed"]
575
- seed = (Integer(seed) rescue nil) unless seed.nil?
576
- clear = body && (body["clear"] == true || body["clear"].to_s == "true")
577
- strict = body && (body["strict"] == true || body["strict"].to_s == "true")
578
- json_response(seed_table_data(table_name, count.to_i, seed: seed, clear: clear, strict: strict))
567
+ handle_seed(env)
579
568
  when ["POST", "/__dev/api/tool"]
580
569
  body = read_json_body(env)
581
570
  tool = (body && body["tool"]) || ""
@@ -590,6 +579,28 @@ module Tina4
590
579
  json_response(run_tests_payload)
591
580
  when ["POST", "/__dev/api/seed/run"]
592
581
  json_response(run_seeds_payload)
582
+ when ["POST", "/__dev/api/query"]
583
+ body = read_json_body(env)
584
+ sql = (body && (body["query"] || body["sql"])) || ""
585
+ json_response(run_query(sql))
586
+ when ["GET", "/__dev/api/tables"]
587
+ json_response(tables_payload)
588
+ when ["GET", "/__dev/api/gallery"]
589
+ json_response(gallery_list)
590
+ when ["POST", "/__dev/api/gallery/deploy"]
591
+ body = read_json_body(env)
592
+ name = (body && body["name"]) || ""
593
+ json_response(gallery_deploy(name))
594
+ when ["GET", "/__dev/api/metrics/full"]
595
+ metrics_full_response
596
+ when ["GET", "/__dev/api/metrics/file"]
597
+ metrics_file_response(env)
598
+ end
599
+ end
600
+
601
+ # Grounding token panel and the database-connection editor.
602
+ def dispatch_config(method, path, env)
603
+ case [method, path]
593
604
  # Grounding panel — configure the tina4-coder MCP token used for
594
605
  # live-docs grounding. Self-contained (.env read/write); no proxy.
595
606
  when ["GET", "/__dev/api/grounding/status"]
@@ -605,40 +616,13 @@ module Tina4
605
616
  when ["POST", "/__dev/api/connections/save"]
606
617
  body = read_json_body(env)
607
618
  handle_connections_save(body)
608
- when ["POST", "/__dev/api/query"]
609
- body = read_json_body(env)
610
- sql = (body && (body["query"] || body["sql"])) || ""
611
- json_response(run_query(sql))
612
- when ["GET", "/__dev/api/tables"]
613
- json_response(tables_payload)
614
- when ["GET", "/__dev/api/gallery"]
615
- json_response(gallery_list)
616
- when ["POST", "/__dev/api/gallery/deploy"]
617
- body = read_json_body(env)
618
- name = (body && body["name"]) || ""
619
- json_response(gallery_deploy(name))
620
- when ["GET", "/__dev/api/version-check"]
621
- json_response(version_check_payload)
622
- when ["GET", "/__dev/api/metrics/full"]
623
- # No fallback (ADR-0002). A missing or stale CLI is a 503 naming the
624
- # install command, never zeros that read as a healthy codebase.
625
- begin
626
- json_response(Tina4::Metrics.full_analysis)
627
- rescue Tina4::MetricsEngineError => e
628
- json_response({ "error" => e.message }, 503)
629
- end
630
- when ["GET", "/__dev/api/metrics/file"]
631
- file_path = (query_param(env, "path") || "").to_s
632
- begin
633
- json_response(Tina4::Metrics.file_detail(file_path))
634
- rescue Tina4::MetricsEngineError => e
635
- # A bad path is the caller's mistake (404); anything else is the
636
- # engine being unavailable (503).
637
- bad_path = e.message.include?("no such file") ||
638
- e.message.include?("not a file") ||
639
- e.message.include?("needs a path")
640
- json_response({ "error" => e.message }, bad_path ? 404 : 503)
641
- end
619
+ end
620
+ end
621
+
622
+ # File browser + editor (read / raw / save / rename / delete) and the
623
+ # dependency search / install surface.
624
+ def dispatch_files(method, path, env)
625
+ case [method, path]
642
626
  when ["GET", "/__dev/api/files"]
643
627
  json_response(files_list(env))
644
628
  when ["GET", "/__dev/api/file"]
@@ -662,8 +646,13 @@ module Tina4
662
646
  when ["POST", "/__dev/api/deps/install"]
663
647
  body = read_json_body(env) || {}
664
648
  json_response(deps_install(body))
665
- when ["GET", "/__dev/api/git/status"]
666
- json_response(git_status_payload)
649
+ end
650
+ end
651
+
652
+ # The MCP surfaces (REST shim + JSON-RPC + SSE), scaffolding, and the
653
+ # live-docs (Live API RAG) + GraphQL-schema endpoints.
654
+ def dispatch_mcp(method, path, env)
655
+ case [method, path]
667
656
  # All four MCP surfaces (REST shim + JSON-RPC + SSE) go through
668
657
  # with_mcp_gate: capability (Tina4.mcp_enabled?) decides whether MCP
669
658
  # runs at all (off → route behaves as unmounted, nil → RackApp 404,
@@ -705,23 +694,133 @@ module Tina4
705
694
  when ["GET", "/__dev/api/docs/.well-known.json"]
706
695
  json_response(docs_well_known_payload)
707
696
  when ["GET", "/__dev/api/graphql/schema"]
708
- begin
709
- gql = Tina4::GraphQL.new
710
- # Auto-discover and register all ORM subclasses
711
- ObjectSpace.each_object(Class).select { |c| c < Tina4::ORM }.each do |model_class|
712
- gql.from_orm(model_class.new)
713
- end
714
- json_response({ schema: gql.introspect, sdl: gql.schema_sdl })
715
- rescue => e
716
- json_response({ error: e.message }, 400)
717
- end
718
- else
719
- nil
697
+ graphql_schema_response
720
698
  end
721
699
  end
722
700
 
701
+ # POST /__dev/api/reload — external reload trigger. Bumps the mtime
702
+ # counter, re-discovers routes, keeps the live docs index fresh, and
703
+ # broadcasts a WebSocket reload to connected browsers.
704
+ def handle_reload(env)
705
+ body = read_json_body(env) || {}
706
+ @reload_mtime = Time.now.to_i
707
+ @reload_file = body["file"] || ""
708
+ reload_type = body["type"] || "reload"
709
+ Tina4::Log.info("External reload trigger: #{reload_type}#{@reload_file.empty? ? '' : " (#{@reload_file})"}")
710
+ # Re-discover so files dropped into src/routes/ register without
711
+ # a server restart. Idempotent — already-loaded files are skipped,
712
+ # changed files are re-loaded (mtime-tracked).
713
+ begin
714
+ Tina4::Router.rescan_routes!
715
+ rescue StandardError => e
716
+ Tina4::Log.error("Re-discover on reload failed: #{e.message}")
717
+ end
718
+ # Keep the code Context index LIVE on the same reload trigger: reindex
719
+ # just the changed file (UPSERT) so the dev-MCP code_search reflects
720
+ # the edit immediately. Only touches an already-built index
721
+ # (existing_context never creates one); guarded so a context failure
722
+ # never breaks the reload.
723
+ begin
724
+ unless @reload_file.to_s.empty?
725
+ ctx = Tina4::Context.existing_context
726
+ ctx&.reindex_file(@reload_file)
727
+ end
728
+ rescue StandardError => e
729
+ Tina4::Log.error("Context reindex on reload failed: #{e.message}")
730
+ end
731
+ # WebSocket-primary reload: push an instant message to every browser
732
+ # connected on /__dev_reload. The toolbar client (and the dev-admin
733
+ # dashboard) act on this immediately — the mtime poll above is only a
734
+ # fallback for when the socket is down. CSS changes swap stylesheets;
735
+ # everything else triggers a full page reload. We normalise the wire
736
+ # `type` to "css"/"reload" (the clients only react to css/reload/change)
737
+ # but still echo the caller's original type in the HTTP response.
738
+ # Wrapped so a broadcast failure — or zero connected clients — never
739
+ # 500s the reload endpoint.
740
+ begin
741
+ ws_type = reload_type == "css" ? "css" : "reload"
742
+ Tina4::DevReload.broadcast(
743
+ JSON.generate({ type: ws_type, file: @reload_file, mtime: @reload_mtime })
744
+ )
745
+ rescue StandardError => e
746
+ Tina4::Log.error("Dev-reload WebSocket broadcast failed: #{e.message}")
747
+ end
748
+ json_response({ ok: true, type: reload_type })
749
+ end
723
750
 
724
- private
751
+ # GET /__dev/api/queue — per-topic stats + job list. Queue#size is
752
+ # keyword-only; the rescue swallows a backend error into empty stats.
753
+ def queue_overview(env)
754
+ topic = query_param(env, "topic") || "default"
755
+ stats = { pending: 0, completed: 0, failed: 0, reserved: 0 }
756
+ jobs = []
757
+ begin
758
+ if defined?(Tina4::Queue)
759
+ queue = Tina4::Queue.new(backend: :file, topic: topic)
760
+ # Queue#size is keyword-only (def size(status:)). Calling it
761
+ # positionally raises ArgumentError, which the rescue below
762
+ # swallows — silently zeroing every stat. Use keyword form.
763
+ stats = {
764
+ pending: queue.size(status: "pending"),
765
+ completed: queue.size(status: "completed"),
766
+ failed: queue.size(status: "failed"),
767
+ reserved: queue.size(status: "reserved"),
768
+ }
769
+ jobs = queue_jobs(topic, query_param(env, "status"))
770
+ end
771
+ rescue StandardError
772
+ # fall through to empty stats
773
+ end
774
+ json_response({ jobs: jobs, stats: stats })
775
+ end
776
+
777
+ # POST /__dev/api/seed — seed fake rows into a table via the shared
778
+ # resilient seed_table helper.
779
+ def handle_seed(env)
780
+ body = read_json_body(env)
781
+ table_name = (body && body["table"]) || ""
782
+ count = (body && body["count"]) || 10
783
+ seed = body && body["seed"]
784
+ seed = (Integer(seed) rescue nil) unless seed.nil?
785
+ clear = body && (body["clear"] == true || body["clear"].to_s == "true")
786
+ strict = body && (body["strict"] == true || body["strict"].to_s == "true")
787
+ json_response(seed_table_data(table_name, count.to_i, seed: seed, clear: clear, strict: strict))
788
+ end
789
+
790
+ # GET /__dev/api/metrics/full — native metrics (ADR-0002). No fallback:
791
+ # a missing/stale CLI is a 503 naming the install command, never zeros
792
+ # that read as a healthy codebase.
793
+ def metrics_full_response
794
+ json_response(Tina4::Metrics.full_analysis)
795
+ rescue Tina4::MetricsEngineError => e
796
+ json_response({ "error" => e.message }, 503)
797
+ end
798
+
799
+ # GET /__dev/api/metrics/file — metrics for one file. A bad path is the
800
+ # caller's mistake (404); anything else is the engine being unavailable
801
+ # (503).
802
+ def metrics_file_response(env)
803
+ file_path = (query_param(env, "path") || "").to_s
804
+ json_response(Tina4::Metrics.file_detail(file_path))
805
+ rescue Tina4::MetricsEngineError => e
806
+ bad_path = e.message.include?("no such file") ||
807
+ e.message.include?("not a file") ||
808
+ e.message.include?("needs a path")
809
+ json_response({ "error" => e.message }, bad_path ? 404 : 503)
810
+ end
811
+
812
+ # GET /__dev/api/graphql/schema — introspect every ORM subclass into a
813
+ # GraphQL schema + SDL.
814
+ def graphql_schema_response
815
+ gql = Tina4::GraphQL.new
816
+ # Auto-discover and register all ORM subclasses
817
+ ObjectSpace.each_object(Class).select { |c| c < Tina4::ORM }.each do |model_class|
818
+ gql.from_orm(model_class.new)
819
+ end
820
+ json_response({ schema: gql.introspect, sdl: gql.schema_sdl })
821
+ rescue => e
822
+ json_response({ error: e.message }, 400)
823
+ end
725
824
 
726
825
  def query_param(env, key)
727
826
  qs = env["QUERY_STRING"] || ""
data/lib/tina4/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.132"
4
+ VERSION = "3.13.133"
5
5
  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.132
4
+ version: 3.13.133
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-09-04 00:00:00.000000000 Z
11
+ date: 2026-09-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack