tina4ruby 3.13.101 → 3.13.103

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: 9102cf347865953d60e7d47b1ead3b87e25f139f1439a19e5f230ec31594c32a
4
- data.tar.gz: 7a5323f6a9d26e41852269c63d76fac619a59b549e634aad6a56bdc887dcb780
3
+ metadata.gz: 6097dca4375329be7c8539cf9e40a8cde39fd9a6041169e58b9edf6bcbcf491a
4
+ data.tar.gz: 9d90b71f00d6a59a151a5a3108319aed79faccaac0858f40c3572825765f0acf
5
5
  SHA512:
6
- metadata.gz: b0d72050d8db6db2ca5a1c621c6212848ef094ab84a3718b32aa06d53912c74c0324973374b1dafc6dd43d64b52dd34cb574a806fe79f034ac8757c0400db1cb
7
- data.tar.gz: b13a9d5139f751391a684e28858bc894268fbb511ca192615eb48181dda59784faddcebcbdf6ae74aefb08736a53e9e356d432bc0ec4f6b69b50132c75a6e3f3
6
+ metadata.gz: 74d5064e7aca1cffcdfd0389b6662a4f2d921ed18de43f8d9f0741bffa6a8ceb7d381af281b44b9e114237d7c783068c1adef1b709eb8c36efa017aa6eeb633e
7
+ data.tar.gz: 13fd4fa1ea1e667eee942d516cd48c14a2488e686ace7d9597d3affe368d2e7ad8e86772367b8d5ed5b3cc94b9a5631a41d62694abf055999ccbab30c9efad99
data/CHANGELOG.md CHANGED
@@ -6,7 +6,35 @@ 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.101
9
+ ## 3.13.103
10
+
11
+ ### Metrics reports what it can prove
12
+
13
+ - Require signed Tina4 client 3.8.76 for the native metrics handoff.
14
+ - Expose `has_referencing_test` as a source-reference signal. It does not claim
15
+ that a test ran or that coverage exists.
16
+ - Fail when the native client is stale instead of falling back to a second
17
+ framework-owned analyser.
18
+
19
+ ### Frond stays stable and gets smaller
20
+
21
+ - Split expression parsing and evaluation into focused internal steps.
22
+ - Preserve public APIs and the shared 84-case expression corpus across all four
23
+ languages.
24
+
25
+ ### One client starts every project
26
+
27
+ - Lead framework skills with `tina4 init` and `tina4 serve`.
28
+ - Keep scaffolding guidance visible and separate runtime dependencies from
29
+ language extensions.
30
+
31
+ ### Release integrity
32
+
33
+ - Align source, runtime, package, lockfile, and AI-facing guide versions.
34
+ - Reject a release tag that does not match the source package version before
35
+ any registry publish begins.
36
+
37
+ +## 3.13.101
10
38
 
11
39
  ### Breaking: metrics has one owner
12
40
 
data/lib/tina4/frond.rb CHANGED
@@ -1566,6 +1566,17 @@ module Tina4
1566
1566
  # FIFO drop-oldest-half, deliberately not LRU (no per-hit bookkeeping on the
1567
1567
  # hot path).
1568
1568
  EXPR_FORM_CACHE_MAX = 2048
1569
+ EXPR_FORM_HANDLERS = {
1570
+ pipe: [:eval_filter_pipe, :not_filter_pipe, true],
1571
+ literal: [:eval_literal, :not_literal, false],
1572
+ function: [:eval_function_call, :not_function, true],
1573
+ concat: [:eval_concat, :not_concat, true],
1574
+ arithmetic: [:eval_arithmetic, :not_arithmetic, true],
1575
+ ternary: [:eval_ternary, :not_ternary, true],
1576
+ inline_if: [:eval_inline_if, :not_inline_if, true],
1577
+ coalesce: [:eval_null_coalesce, :not_coalesce, true],
1578
+ collection: [:eval_collection_literal, :not_collection, true]
1579
+ }.freeze
1569
1580
 
1570
1581
  # ── Expression evaluation: a cascade, with the branch memoised ──
1571
1582
  #
@@ -1648,21 +1659,16 @@ module Tina4
1648
1659
  # Evaluate via the one remembered branch. Returns :form_declined when that
1649
1660
  # branch no longer applies, so the caller re-runs the full cascade.
1650
1661
  def eval_expr_as(form, expr, context)
1651
- case form
1652
- when :resolve then resolve(expr, context)
1653
- when :pipe then (r = eval_filter_pipe(expr, context)) == :not_filter_pipe ? :form_declined : r
1654
- when :literal then (r = eval_literal(expr)) == :not_literal ? :form_declined : r
1655
- when :function then (r = eval_function_call(expr, context)) == :not_function ? :form_declined : r
1656
- when :concat then (r = eval_concat(expr, context)) == :not_concat ? :form_declined : r
1657
- when :comparison then has_comparison?(expr) ? eval_comparison(expr, context) : :form_declined
1658
- when :arithmetic then (r = eval_arithmetic(expr, context)) == :not_arithmetic ? :form_declined : r
1659
- when :ternary then (r = eval_ternary(expr, context)) == :not_ternary ? :form_declined : r
1660
- when :inline_if then (r = eval_inline_if(expr, context)) == :not_inline_if ? :form_declined : r
1661
- when :coalesce then (r = eval_null_coalesce(expr, context)) == :not_coalesce ? :form_declined : r
1662
- when :collection then (r = eval_collection_literal(expr, context)) == :not_collection ? :form_declined : r
1663
- when :parens then matched_parens?(expr) ? eval_expr(expr[1..-2], context) : :form_declined
1664
- else :form_declined
1665
- end
1662
+ return resolve(expr, context) if form == :resolve
1663
+ return has_comparison?(expr) ? eval_comparison(expr, context) : :form_declined if form == :comparison
1664
+ return matched_parens?(expr) ? eval_expr(expr[1..-2], context) : :form_declined if form == :parens
1665
+
1666
+ handler = EXPR_FORM_HANDLERS[form]
1667
+ return :form_declined unless handler
1668
+
1669
+ method_name, declined, needs_context = handler
1670
+ result = needs_context ? send(method_name, expr, context) : send(method_name, expr)
1671
+ result == declined ? :form_declined : result
1666
1672
  end
1667
1673
 
1668
1674
  # ── Filter pipe: value|filter(args) ──
data/lib/tina4/metrics.rb CHANGED
@@ -11,7 +11,7 @@ module Tina4
11
11
  module Metrics
12
12
  INSTALL_HINT = 'update the native tina4 CLI: https://tina4.com/cli'
13
13
  SUMMARY_KEYS = %w[files_analyzed total_functions avg_complexity avg_maintainability].freeze
14
- FILE_KEYS = %w[path loc avg_complexity maintainability has_tests].freeze
14
+ FILE_KEYS = %w[path loc avg_complexity maintainability has_referencing_test].freeze
15
15
  FUNCTION_KEYS = %w[name file line complexity loc].freeze
16
16
 
17
17
  def self.resolve_target(root = 'src')
@@ -234,11 +234,11 @@ ${t.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
234
234
  </tr>`).join("")}
235
235
  </tbody>
236
236
  </table>
237
- `)}function Xy(i,e){const t=[],n={},r={};i.forEach((s,o)=>{n[Rd(s)]=o,r[s]=o});for(const[s,o]of Object.entries(e)){const a=r[s];if(a!==void 0)for(const l of o){const O=r[l];if(O!==void 0){O!==a&&t.push([a,O]);continue}const c=l.replace(/^\.\//,"").replace(/^\.\.\//,"").split(/[./]/);let d;for(let h=c.length-1;h>=0;h--){const f=c[h].toLowerCase();if(f&&f!=="js"&&f!=="py"&&f!=="rb"&&f!=="ts"&&f!=="index"&&(d=n[f],d!==void 0))break}d===void 0&&(d=n[Rd(l)]),d!==void 0&&a!==d&&t.push([a,d])}}return t}function Rd(i){const e=i.replace(/\\/g,"/").split("/").pop()||"",t=e.lastIndexOf(".");return(t>0?e.substring(0,t):e).toLowerCase()}function _y(i,e,t,n){var gi,$n,ps;const r=e.offsetWidth||900,s=Math.max(450,Math.min(650,r*.45)),o=Math.max(...i.map(G=>G.loc))||1,a=Math.max(...i.map(G=>G.dep_count||0))||1,l=Math.max(...i.map(G=>G.coupling_afferent||0))||1,O=i.some(G=>(G.coupling_afferent||0)>0),c=14,d=Math.min(70,r/10);function h(G){const I=Math.min((G.avg_complexity||0)/10,1),D=G.has_tests?0:1,de=Math.min((G.dep_count||0)/5,1),j=I*.4+D*.4+de*.2,W=Math.max(0,Math.min(1,j)),H=Math.round(120*(1-W)),te=Math.round(70+W*30),pe=Math.round(42+18*(1-W));return`hsl(${H},${te}%,${pe}%)`}function f(G){return G.loc/o*.4+(G.avg_complexity||0)/10*.4+(G.dep_count||0)/a*.2}const p=[...i].sort((G,I)=>f(G)-f(I)),m=r/2,$=s/2,g=[];let y=0,S=0;for(const G of p){const I=c+Math.sqrt(f(G))*(d-c),D=h(G);let de=!1;for(let j=0;j<800;j++){const W=m+S*Math.cos(y),H=$+S*Math.sin(y);let te=!1;for(const pe of g){const he=W-pe.x,ke=H-pe.y;if(Math.sqrt(he*he+ke*ke)<I+pe.r+2){te=!0;break}}if(!te&&W>I+2&&W<r-I-2&&H>I+25&&H<s-I-2){g.push({x:W,y:H,vx:0,vy:0,r:I,color:D,f:G}),de=!0;break}y+=.2,S+=.04}de||g.push({x:m+(Math.random()-.5)*r*.3,y:$+(Math.random()-.5)*s*.3,vx:0,vy:0,r:I,color:D,f:G})}const x=Xy(g.map(G=>G.f.path),t),w=document.createElement("canvas");w.width=r,w.height=s,w.style.cssText="display:block;border:1px solid var(--border);border-radius:8px;cursor:pointer;background:#0f172a";const R=n==="framework"?'<span style="color:#cba6f7;font-weight:600">(Framework)</span> Add code to src/ to see your project':"";e.innerHTML=`<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.5rem"><h3 style="margin:0;font-size:0.85rem">Code Landscape ${R}</h3><span style="font-size:0.65rem;color:var(--muted)">Drag bubbles | Dbl-click to drill down</span></div><div style="position:relative" id="metrics-canvas-wrap"></div><div style="margin-top:0.4rem;font-size:0.62rem;color:var(--muted);display:flex;gap:0.9rem;flex-wrap:wrap">${O?'<span>Centre = most depended-on</span><span><span style="color:hsl(210,85%,60%)">&#9679;</span> stable ring</span><span><span style="color:hsl(30,85%,60%)">&#9679;</span> unstable ring</span>':""}<span id="metrics-hover-info" class="text-mono"></span></div>`,document.getElementById("metrics-canvas-wrap").appendChild(w);const _=document.getElementById("metrics-hover-info"),A=document.createElement("div");A.style.cssText="position:absolute;top:8px;left:8px;z-index:2;display:flex;gap:4px;flex-direction:column",A.innerHTML=`
237
+ `)}function Xy(i,e){const t=[],n={},r={};i.forEach((s,o)=>{n[Rd(s)]=o,r[s]=o});for(const[s,o]of Object.entries(e)){const a=r[s];if(a!==void 0)for(const l of o){const O=r[l];if(O!==void 0){O!==a&&t.push([a,O]);continue}const c=l.replace(/^\.\//,"").replace(/^\.\.\//,"").split(/[./]/);let d;for(let h=c.length-1;h>=0;h--){const f=c[h].toLowerCase();if(f&&f!=="js"&&f!=="py"&&f!=="rb"&&f!=="ts"&&f!=="index"&&(d=n[f],d!==void 0))break}d===void 0&&(d=n[Rd(l)]),d!==void 0&&a!==d&&t.push([a,d])}}return t}function Rd(i){const e=i.replace(/\\/g,"/").split("/").pop()||"",t=e.lastIndexOf(".");return(t>0?e.substring(0,t):e).toLowerCase()}function _y(i,e,t,n){var gi,$n,ps;const r=e.offsetWidth||900,s=Math.max(450,Math.min(650,r*.45)),o=Math.max(...i.map(G=>G.loc))||1,a=Math.max(...i.map(G=>G.dep_count||0))||1,l=Math.max(...i.map(G=>G.coupling_afferent||0))||1,O=i.some(G=>(G.coupling_afferent||0)>0),c=14,d=Math.min(70,r/10);function h(G){const I=Math.min((G.avg_complexity||0)/10,1),D=G.has_referencing_test?0:1,de=Math.min((G.dep_count||0)/5,1),j=I*.4+D*.4+de*.2,W=Math.max(0,Math.min(1,j)),H=Math.round(120*(1-W)),te=Math.round(70+W*30),pe=Math.round(42+18*(1-W));return`hsl(${H},${te}%,${pe}%)`}function f(G){return G.loc/o*.4+(G.avg_complexity||0)/10*.4+(G.dep_count||0)/a*.2}const p=[...i].sort((G,I)=>f(G)-f(I)),m=r/2,$=s/2,g=[];let y=0,S=0;for(const G of p){const I=c+Math.sqrt(f(G))*(d-c),D=h(G);let de=!1;for(let j=0;j<800;j++){const W=m+S*Math.cos(y),H=$+S*Math.sin(y);let te=!1;for(const pe of g){const he=W-pe.x,ke=H-pe.y;if(Math.sqrt(he*he+ke*ke)<I+pe.r+2){te=!0;break}}if(!te&&W>I+2&&W<r-I-2&&H>I+25&&H<s-I-2){g.push({x:W,y:H,vx:0,vy:0,r:I,color:D,f:G}),de=!0;break}y+=.2,S+=.04}de||g.push({x:m+(Math.random()-.5)*r*.3,y:$+(Math.random()-.5)*s*.3,vx:0,vy:0,r:I,color:D,f:G})}const x=Xy(g.map(G=>G.f.path),t),w=document.createElement("canvas");w.width=r,w.height=s,w.style.cssText="display:block;border:1px solid var(--border);border-radius:8px;cursor:pointer;background:#0f172a";const R=n==="framework"?'<span style="color:#cba6f7;font-weight:600">(Framework)</span> Add code to src/ to see your project':"";e.innerHTML=`<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.5rem"><h3 style="margin:0;font-size:0.85rem">Code Landscape ${R}</h3><span style="font-size:0.65rem;color:var(--muted)">Drag bubbles | Dbl-click to drill down</span></div><div style="position:relative" id="metrics-canvas-wrap"></div><div style="margin-top:0.4rem;font-size:0.62rem;color:var(--muted);display:flex;gap:0.9rem;flex-wrap:wrap">${O?'<span>Centre = most depended-on</span><span><span style="color:hsl(210,85%,60%)">&#9679;</span> stable ring</span><span><span style="color:hsl(30,85%,60%)">&#9679;</span> unstable ring</span>':""}<span id="metrics-hover-info" class="text-mono"></span></div>`,document.getElementById("metrics-canvas-wrap").appendChild(w);const _=document.getElementById("metrics-hover-info"),A=document.createElement("div");A.style.cssText="position:absolute;top:8px;left:8px;z-index:2;display:flex;gap:4px;flex-direction:column",A.innerHTML=`
238
238
  <button class="btn btn-sm" id="metrics-zoom-in" style="width:28px;height:28px;padding:0;font-size:14px;font-weight:700;line-height:1">+</button>
239
239
  <button class="btn btn-sm" id="metrics-zoom-out" style="width:28px;height:28px;padding:0;font-size:14px;font-weight:700;line-height:1">&minus;</button>
240
240
  <button class="btn btn-sm" id="metrics-zoom-fit" style="width:28px;height:28px;padding:0;font-size:10px;font-weight:700;line-height:1">Fit</button>
241
- `,document.getElementById("metrics-canvas-wrap").appendChild(A),(gi=document.getElementById("metrics-zoom-in"))==null||gi.addEventListener("click",()=>{le=Math.min(5,le*1.3)}),($n=document.getElementById("metrics-zoom-out"))==null||$n.addEventListener("click",()=>{le=Math.max(.3,le*.7)}),(ps=document.getElementById("metrics-zoom-fit"))==null||ps.addEventListener("click",()=>{le=1,U=0,oe=0});const P=w.getContext("2d");let v=-1,k=-1,X=0,z=0,U=0,oe=0,le=1,rt=!1,mn=0,Gt=0,us=0,ft=0;function De(){for(let j=0;j<g.length;j++){if(j===k)continue;const W=g[j],H=m-W.x,te=$-W.y,pe=.3+W.r/d*.7,he=O?.3+(W.f.coupling_afferent||0)/l*.7:pe,ke=.008*pe*he;W.vx+=H*ke,W.vy+=te*ke}for(const[j,W]of x){const H=g[j],te=g[W],pe=te.x-H.x,he=te.y-H.y,ke=Math.sqrt(pe*pe+he*he)||1,Dt=H.r+te.r+20,Bt=(ke-Dt)*.002,ms=pe/ke*Bt,$s=he/ke*Bt;j!==k&&(H.vx+=ms,H.vy+=$s),W!==k&&(te.vx-=ms,te.vy-=$s)}for(let j=0;j<g.length;j++)for(let W=j+1;W<g.length;W++){const H=g[j],te=g[W],pe=te.x-H.x,he=te.y-H.y,ke=Math.sqrt(pe*pe+he*he)||1,Dt=H.r+te.r+20;if(ke<Dt){const Bt=40*(Dt-ke)/Dt,ms=pe/ke*Bt,$s=he/ke*Bt;j!==k&&(H.vx-=ms,H.vy-=$s),W!==k&&(te.vx+=ms,te.vy+=$s)}}for(let j=0;j<g.length;j++){if(j===k)continue;const W=g[j];W.vx*=.65,W.vy*=.65;const H=2;W.vx=Math.max(-H,Math.min(H,W.vx)),W.vy=Math.max(-H,Math.min(H,W.vy)),W.x+=W.vx,W.y+=W.vy,W.x=Math.max(W.r+2,Math.min(r-W.r-2,W.x)),W.y=Math.max(W.r+25,Math.min(s-W.r-2,W.y))}}function st(){var G;De(),P.clearRect(0,0,r,s),P.save(),P.translate(U,oe),P.scale(le,le),P.strokeStyle="rgba(255,255,255,0.03)",P.lineWidth=1/le;for(let I=0;I<r/le;I+=50)P.beginPath(),P.moveTo(I,0),P.lineTo(I,s/le),P.stroke();for(let I=0;I<s/le;I+=50)P.beginPath(),P.moveTo(0,I),P.lineTo(r/le,I),P.stroke();for(const[I,D]of x){const de=g[I],j=g[D],W=j.x-de.x,H=j.y-de.y,te=Math.sqrt(W*W+H*H)||1,pe=v===I||v===D;P.beginPath(),P.moveTo(de.x+W/te*de.r,de.y+H/te*de.r);const he=j.x-W/te*j.r,ke=j.y-H/te*j.r;P.lineTo(he,ke),P.strokeStyle=pe?"rgba(139,180,250,0.9)":"rgba(255,255,255,0.15)",P.lineWidth=pe?3:1,P.stroke();const Dt=pe?12:6,Bt=Math.atan2(H,W);P.beginPath(),P.moveTo(he,ke),P.lineTo(he-Dt*Math.cos(Bt-.4),ke-Dt*Math.sin(Bt-.4)),P.lineTo(he-Dt*Math.cos(Bt+.4),ke-Dt*Math.sin(Bt+.4)),P.closePath(),P.fillStyle=P.strokeStyle,P.fill()}for(let I=0;I<g.length;I++){const D=g[I],de=I===v,j=de?D.r+4:D.r;de&&(P.beginPath(),P.arc(D.x,D.y,j+8,0,Math.PI*2),P.fillStyle="rgba(255,255,255,0.08)",P.fill()),P.beginPath(),P.arc(D.x,D.y,j,0,Math.PI*2),P.fillStyle=D.color,P.globalAlpha=de?1:.85,P.fill(),P.globalAlpha=1,P.strokeStyle=de?"rgba(255,255,255,0.6)":"rgba(255,255,255,0.25)",P.lineWidth=de?2.5:1.5,P.stroke();const W=D.f.instability;O&&typeof W=="number"&&(D.f.coupling_afferent||0)+(D.f.coupling_efferent||0)>0&&(P.beginPath(),P.arc(D.x,D.y,j+3,0,Math.PI*2),P.strokeStyle=`hsl(${Math.round(210-180*W)},85%,60%)`,P.lineWidth=1+W*2.5,P.stroke());const H=((G=D.f.path.split("/").pop())==null?void 0:G.replace(/\.\w+$/,""))||"?";if(j>16){const he=Math.max(8,Math.min(13,j*.38));P.fillStyle="#fff",P.font=`600 ${he}px monospace`,P.textAlign="center",P.fillText(H,D.x,D.y-2),P.fillStyle="rgba(255,255,255,0.65)",P.font=`${he-1}px monospace`,P.fillText(`${D.f.loc} LOC`,D.x,D.y+he)}const te=Math.max(9,j*.3),pe=te*.7;if(j>14&&D.f.dep_count>0){const he=D.y-j+pe+3;P.beginPath(),P.arc(D.x,he,pe,0,Math.PI*2),P.fillStyle="#ea580c",P.fill(),P.fillStyle="#fff",P.font=`bold ${te}px sans-serif`,P.textAlign="center",P.fillText("D",D.x,he+te*.35)}if(j>14&&D.f.has_tests){const he=D.y+j-pe-3;P.beginPath(),P.arc(D.x,he,pe,0,Math.PI*2),P.fillStyle="#16a34a",P.fill(),P.fillStyle="#fff",P.font=`bold ${te}px sans-serif`,P.textAlign="center",P.fillText("T",D.x,he+te*.35)}}if(P.restore(),v>=0){const I=g[v].f,D=I.coupling_afferent??0,de=I.coupling_efferent??0,j=typeof I.instability=="number"?I.instability.toFixed(2):"-";_.textContent=`${I.path} ca ${D} / ce ${de} / I ${j} / MI ${I.maintainability}`}else _.textContent&&(_.textContent="");requestAnimationFrame(st)}w.addEventListener("mousemove",G=>{const I=w.getBoundingClientRect(),D=(G.clientX-I.left-U)/le,de=(G.clientY-I.top-oe)/le;if(rt){U=us+(G.clientX-mn),oe=ft+(G.clientY-Gt);return}if(k>=0){ut=!0,g[k].x=D+X,g[k].y=de+z,g[k].vx=0,g[k].vy=0;return}v=-1;for(let j=g.length-1;j>=0;j--){const W=g[j],H=D-W.x,te=de-W.y;if(Math.sqrt(H*H+te*te)<W.r+4){v=j;break}}w.style.cursor=v>=0?"grab":"default"}),w.addEventListener("mousedown",G=>{const I=w.getBoundingClientRect(),D=(G.clientX-I.left-U)/le,de=(G.clientY-I.top-oe)/le;if(G.button===2){rt=!0,mn=G.clientX,Gt=G.clientY,us=U,ft=oe,w.style.cursor="move";return}v>=0&&(k=v,X=g[k].x-D,z=g[k].y-de,ut=!1,w.style.cursor="grabbing")});let ut=!1;w.addEventListener("mouseup",G=>{if(rt){rt=!1,w.style.cursor="default";return}if(k>=0){ut||Xa(g[k].f.path),w.style.cursor="grab",k=-1,ut=!1;return}}),w.addEventListener("mouseleave",()=>{v=-1,k=-1,rt=!1}),w.addEventListener("dblclick",G=>{const I=w.getBoundingClientRect(),D=(G.clientX-I.left-U)/le,de=(G.clientY-I.top-oe)/le;for(let j=g.length-1;j>=0;j--){const W=g[j],H=D-W.x,te=de-W.y;if(Math.sqrt(H*H+te*te)<W.r+4){Xa(W.f.path);break}}}),w.addEventListener("contextmenu",G=>G.preventDefault()),requestAnimationFrame(st)}async function Xa(i){const e=document.getElementById("metrics-detail");if(!e)return;e.innerHTML='<p class="text-muted">Loading file analysis...</p>';const t=await L("/metrics/file?path="+encodeURIComponent(i));if(t.error){e.innerHTML=`<p style="color:var(--danger)">${Q(t.error)}</p>`;return}const n=t.functions||[],r=Math.max(1,...n.map(s=>s.complexity));e.innerHTML=`
241
+ `,document.getElementById("metrics-canvas-wrap").appendChild(A),(gi=document.getElementById("metrics-zoom-in"))==null||gi.addEventListener("click",()=>{le=Math.min(5,le*1.3)}),($n=document.getElementById("metrics-zoom-out"))==null||$n.addEventListener("click",()=>{le=Math.max(.3,le*.7)}),(ps=document.getElementById("metrics-zoom-fit"))==null||ps.addEventListener("click",()=>{le=1,U=0,oe=0});const P=w.getContext("2d");let v=-1,k=-1,X=0,z=0,U=0,oe=0,le=1,rt=!1,mn=0,Gt=0,us=0,ft=0;function De(){for(let j=0;j<g.length;j++){if(j===k)continue;const W=g[j],H=m-W.x,te=$-W.y,pe=.3+W.r/d*.7,he=O?.3+(W.f.coupling_afferent||0)/l*.7:pe,ke=.008*pe*he;W.vx+=H*ke,W.vy+=te*ke}for(const[j,W]of x){const H=g[j],te=g[W],pe=te.x-H.x,he=te.y-H.y,ke=Math.sqrt(pe*pe+he*he)||1,Dt=H.r+te.r+20,Bt=(ke-Dt)*.002,ms=pe/ke*Bt,$s=he/ke*Bt;j!==k&&(H.vx+=ms,H.vy+=$s),W!==k&&(te.vx-=ms,te.vy-=$s)}for(let j=0;j<g.length;j++)for(let W=j+1;W<g.length;W++){const H=g[j],te=g[W],pe=te.x-H.x,he=te.y-H.y,ke=Math.sqrt(pe*pe+he*he)||1,Dt=H.r+te.r+20;if(ke<Dt){const Bt=40*(Dt-ke)/Dt,ms=pe/ke*Bt,$s=he/ke*Bt;j!==k&&(H.vx-=ms,H.vy-=$s),W!==k&&(te.vx+=ms,te.vy+=$s)}}for(let j=0;j<g.length;j++){if(j===k)continue;const W=g[j];W.vx*=.65,W.vy*=.65;const H=2;W.vx=Math.max(-H,Math.min(H,W.vx)),W.vy=Math.max(-H,Math.min(H,W.vy)),W.x+=W.vx,W.y+=W.vy,W.x=Math.max(W.r+2,Math.min(r-W.r-2,W.x)),W.y=Math.max(W.r+25,Math.min(s-W.r-2,W.y))}}function st(){var G;De(),P.clearRect(0,0,r,s),P.save(),P.translate(U,oe),P.scale(le,le),P.strokeStyle="rgba(255,255,255,0.03)",P.lineWidth=1/le;for(let I=0;I<r/le;I+=50)P.beginPath(),P.moveTo(I,0),P.lineTo(I,s/le),P.stroke();for(let I=0;I<s/le;I+=50)P.beginPath(),P.moveTo(0,I),P.lineTo(r/le,I),P.stroke();for(const[I,D]of x){const de=g[I],j=g[D],W=j.x-de.x,H=j.y-de.y,te=Math.sqrt(W*W+H*H)||1,pe=v===I||v===D;P.beginPath(),P.moveTo(de.x+W/te*de.r,de.y+H/te*de.r);const he=j.x-W/te*j.r,ke=j.y-H/te*j.r;P.lineTo(he,ke),P.strokeStyle=pe?"rgba(139,180,250,0.9)":"rgba(255,255,255,0.15)",P.lineWidth=pe?3:1,P.stroke();const Dt=pe?12:6,Bt=Math.atan2(H,W);P.beginPath(),P.moveTo(he,ke),P.lineTo(he-Dt*Math.cos(Bt-.4),ke-Dt*Math.sin(Bt-.4)),P.lineTo(he-Dt*Math.cos(Bt+.4),ke-Dt*Math.sin(Bt+.4)),P.closePath(),P.fillStyle=P.strokeStyle,P.fill()}for(let I=0;I<g.length;I++){const D=g[I],de=I===v,j=de?D.r+4:D.r;de&&(P.beginPath(),P.arc(D.x,D.y,j+8,0,Math.PI*2),P.fillStyle="rgba(255,255,255,0.08)",P.fill()),P.beginPath(),P.arc(D.x,D.y,j,0,Math.PI*2),P.fillStyle=D.color,P.globalAlpha=de?1:.85,P.fill(),P.globalAlpha=1,P.strokeStyle=de?"rgba(255,255,255,0.6)":"rgba(255,255,255,0.25)",P.lineWidth=de?2.5:1.5,P.stroke();const W=D.f.instability;O&&typeof W=="number"&&(D.f.coupling_afferent||0)+(D.f.coupling_efferent||0)>0&&(P.beginPath(),P.arc(D.x,D.y,j+3,0,Math.PI*2),P.strokeStyle=`hsl(${Math.round(210-180*W)},85%,60%)`,P.lineWidth=1+W*2.5,P.stroke());const H=((G=D.f.path.split("/").pop())==null?void 0:G.replace(/\.\w+$/,""))||"?";if(j>16){const he=Math.max(8,Math.min(13,j*.38));P.fillStyle="#fff",P.font=`600 ${he}px monospace`,P.textAlign="center",P.fillText(H,D.x,D.y-2),P.fillStyle="rgba(255,255,255,0.65)",P.font=`${he-1}px monospace`,P.fillText(`${D.f.loc} LOC`,D.x,D.y+he)}const te=Math.max(9,j*.3),pe=te*.7;if(j>14&&D.f.dep_count>0){const he=D.y-j+pe+3;P.beginPath(),P.arc(D.x,he,pe,0,Math.PI*2),P.fillStyle="#ea580c",P.fill(),P.fillStyle="#fff",P.font=`bold ${te}px sans-serif`,P.textAlign="center",P.fillText("D",D.x,he+te*.35)}if(j>14&&D.f.has_referencing_test){const he=D.y+j-pe-3;P.beginPath(),P.arc(D.x,he,pe,0,Math.PI*2),P.fillStyle="#16a34a",P.fill(),P.fillStyle="#fff",P.font=`bold ${te}px sans-serif`,P.textAlign="center",P.fillText("T",D.x,he+te*.35)}}if(P.restore(),v>=0){const I=g[v].f,D=I.coupling_afferent??0,de=I.coupling_efferent??0,j=typeof I.instability=="number"?I.instability.toFixed(2):"-";_.textContent=`${I.path} ca ${D} / ce ${de} / I ${j} / MI ${I.maintainability}`}else _.textContent&&(_.textContent="");requestAnimationFrame(st)}w.addEventListener("mousemove",G=>{const I=w.getBoundingClientRect(),D=(G.clientX-I.left-U)/le,de=(G.clientY-I.top-oe)/le;if(rt){U=us+(G.clientX-mn),oe=ft+(G.clientY-Gt);return}if(k>=0){ut=!0,g[k].x=D+X,g[k].y=de+z,g[k].vx=0,g[k].vy=0;return}v=-1;for(let j=g.length-1;j>=0;j--){const W=g[j],H=D-W.x,te=de-W.y;if(Math.sqrt(H*H+te*te)<W.r+4){v=j;break}}w.style.cursor=v>=0?"grab":"default"}),w.addEventListener("mousedown",G=>{const I=w.getBoundingClientRect(),D=(G.clientX-I.left-U)/le,de=(G.clientY-I.top-oe)/le;if(G.button===2){rt=!0,mn=G.clientX,Gt=G.clientY,us=U,ft=oe,w.style.cursor="move";return}v>=0&&(k=v,X=g[k].x-D,z=g[k].y-de,ut=!1,w.style.cursor="grabbing")});let ut=!1;w.addEventListener("mouseup",G=>{if(rt){rt=!1,w.style.cursor="default";return}if(k>=0){ut||Xa(g[k].f.path),w.style.cursor="grab",k=-1,ut=!1;return}}),w.addEventListener("mouseleave",()=>{v=-1,k=-1,rt=!1}),w.addEventListener("dblclick",G=>{const I=w.getBoundingClientRect(),D=(G.clientX-I.left-U)/le,de=(G.clientY-I.top-oe)/le;for(let j=g.length-1;j>=0;j--){const W=g[j],H=D-W.x,te=de-W.y;if(Math.sqrt(H*H+te*te)<W.r+4){Xa(W.f.path);break}}}),w.addEventListener("contextmenu",G=>G.preventDefault()),requestAnimationFrame(st)}async function Xa(i){const e=document.getElementById("metrics-detail");if(!e)return;e.innerHTML='<p class="text-muted">Loading file analysis...</p>';const t=await L("/metrics/file?path="+encodeURIComponent(i));if(t.error){e.innerHTML=`<p style="color:var(--danger)">${Q(t.error)}</p>`;return}const n=t.functions||[],r=Math.max(1,...n.map(s=>s.complexity));e.innerHTML=`
242
242
  <div style="background:var(--surface);border:1px solid var(--border);border-radius:0.5rem;padding:1rem">
243
243
  <div class="flex items-center" style="justify-content:space-between;margin-bottom:0.75rem">
244
244
  <h3 style="font-size:0.9rem">${Q(t.path)}</h3>
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.101"
4
+ VERSION = "3.13.103"
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.101
4
+ version: 3.13.103
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-08-14 00:00:00.000000000 Z
11
+ date: 2026-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -304,10 +304,8 @@ dependencies:
304
304
  - - "~>"
305
305
  - !ruby/object:Gem::Version
306
306
  version: '0.10'
307
- description: 'TINA4: The Intelligent Native Application 4ramework for Ruby Simple.
308
- Fast. Human. Built for AI. Built for you. 54 built-in features, minimal dependencies
309
- (Rack-based; the runtime gems are production-deployment essentials, not framework
310
- bloat). Full parity with Python, PHP, and Node.js.'
307
+ description: 'TINA4: The Intelligent Native Application 4ramework for Ruby. A Rack-based
308
+ backend with native Ruby conventions and shared cross-language contracts.'
311
309
  email:
312
310
  - info@tina4.com
313
311
  executables:
@@ -494,5 +492,5 @@ requirements: []
494
492
  rubygems_version: 3.4.19
495
493
  signing_key:
496
494
  specification_version: 4
497
- summary: Tina4 for Ruby — 54 built-in features, minimal dependencies
495
+ summary: Tina4 for Ruby — native Ruby conventions and shared Tina4 contracts
498
496
  test_files: []