diff --git a/CoderMind/.gitignore b/CoderMind/.gitignore index 835fb40..298698b 100644 --- a/CoderMind/.gitignore +++ b/CoderMind/.gitignore @@ -239,3 +239,5 @@ plans/ .mcp.json .github/agents/ .github/prompts/ + +.claude/commands/ diff --git a/CoderMind/pyproject.toml b/CoderMind/pyproject.toml index f73a08c..7485fd2 100644 --- a/CoderMind/pyproject.toml +++ b/CoderMind/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cmind-cli" -version = "0.1.9" +version = "0.1.10" description = "CoderMind CLI - A tool to generate feature trees for repository planning and code generation." requires-python = ">=3.12" dependencies = [ diff --git a/CoderMind/scripts/check_code_gen.py b/CoderMind/scripts/check_code_gen.py index f2165ec..5a0da18 100644 --- a/CoderMind/scripts/check_code_gen.py +++ b/CoderMind/scripts/check_code_gen.py @@ -13,9 +13,14 @@ import json import argparse +import sys from pathlib import Path from typing import Dict, Any, List, Tuple +SCRIPTS_DIR = Path(__file__).resolve().parent +if str(SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPTS_DIR)) + # Import centralized paths and state loader from common.paths import ( TASKS_FILE, @@ -24,6 +29,8 @@ cmd_for, REPO_DIR, ) +from common.run_events import ArtifactEvent, CommandRun, StepEvent, VerificationEvent +from common.run_report import write_command_report from common.execution_state import load_code_gen_state from common.execution_state import load_code_gen_state as _load_state, save_code_gen_state as _save_state from common.execution_state import complete_batch as _complete_batch @@ -427,6 +434,46 @@ def determine_state( return result +def _write_code_gen_report(result: Dict[str, Any]) -> str | None: + try: + stats = result.get("stats") or {} + report_path = write_command_report(CommandRun( + command="code_gen", + title="CoderMind code_gen Progress View", + status=result.get("type"), + summary=[ + {"label": "state", "value": result.get("type", "")}, + {"label": "total", "value": stats.get("total_tasks", 0)}, + {"label": "completed", "value": stats.get("completed", 0)}, + {"label": "failed", "value": stats.get("failed", 0)}, + {"label": "remaining", "value": stats.get("remaining", 0)}, + {"label": "current batch", "value": (result.get("current_batch") or {}).get("batch_id", "")}, + {"label": "next batch", "value": result.get("next_batch", "")}, + ], + steps=[ + StepEvent(name="determine_state", status=result.get("type"), reason=result.get("message", "")), + StepEvent(name="current_batch", status=(result.get("current_batch") or {}).get("phase", "none"), reason=(result.get("current_batch") or {}).get("file_path", "")), + StepEvent(name="next_action", status="available" if result.get("next_action") else "missing", reason=result.get("next_action", "")), + ], + artifacts=[ + ArtifactEvent(label="tasks", path=TASKS_FILE), + ArtifactEvent(label="code_gen_state", path=STATE_FILE), + ], + verification=[VerificationEvent(name="state", status=result.get("type"), detail=result.get("message", ""))], + evidence={ + "type": result.get("type"), + "stats": stats, + "current_batch": result.get("current_batch"), + "next_batch": result.get("next_batch"), + "next_action": result.get("next_action"), + }, + )) + return str(report_path) + except Exception as exc: + result["report_error"] = str(exc) + return None + + def print_status(result: Dict[str, Any], json_output: bool = False) -> None: """Print the status in human-readable or JSON format.""" if json_output: @@ -474,7 +521,9 @@ def print_status(result: Dict[str, Any], json_output: bool = False) -> None: # Next batch info if result.get("next_batch"): print(f"\n Next Batch: {result['next_batch']}") - + if result.get("report_path"): + print(f"\n Report: {result['report_path']}") + # Guidance print("\n " + "─" * 60) @@ -516,6 +565,9 @@ def main(): args = parser.parse_args() result = determine_state(args.tasks, args.state) + report_path = _write_code_gen_report(result) + if report_path: + result["report_path"] = report_path print_status(result, json_output=args.json) # Return exit code based on state diff --git a/CoderMind/scripts/common/assets/d3.v7.min.js b/CoderMind/scripts/common/assets/d3.v7.min.js new file mode 100644 index 0000000..33bb880 --- /dev/null +++ b/CoderMind/scripts/common/assets/d3.v7.min.js @@ -0,0 +1,2 @@ +// https://d3js.org v7.9.0 Copyright 2010-2023 Mike Bostock +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).d3=t.d3||{})}(this,(function(t){"use strict";function n(t,n){return null==t||null==n?NaN:tn?1:t>=n?0:NaN}function e(t,n){return null==t||null==n?NaN:nt?1:n>=t?0:NaN}function r(t){let r,o,a;function u(t,n,e=0,i=t.length){if(e>>1;o(t[r],n)<0?e=r+1:i=r}while(en(t(e),r),a=(n,e)=>t(n)-e):(r=t===n||t===e?t:i,o=t,a=t),{left:u,center:function(t,n,e=0,r=t.length){const i=u(t,n,e,r-1);return i>e&&a(t[i-1],n)>-a(t[i],n)?i-1:i},right:function(t,n,e=0,i=t.length){if(e>>1;o(t[r],n)<=0?e=r+1:i=r}while(e{n(t,e,(r<<=2)+0,(i<<=2)+0,o<<=2),n(t,e,r+1,i+1,o),n(t,e,r+2,i+2,o),n(t,e,r+3,i+3,o)}}));function d(t){return function(n,e,r=e){if(!((e=+e)>=0))throw new RangeError("invalid rx");if(!((r=+r)>=0))throw new RangeError("invalid ry");let{data:i,width:o,height:a}=n;if(!((o=Math.floor(o))>=0))throw new RangeError("invalid width");if(!((a=Math.floor(void 0!==a?a:i.length/o))>=0))throw new RangeError("invalid height");if(!o||!a||!e&&!r)return n;const u=e&&t(e),c=r&&t(r),f=i.slice();return u&&c?(p(u,f,i,o,a),p(u,i,f,o,a),p(u,f,i,o,a),g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)):u?(p(u,i,f,o,a),p(u,f,i,o,a),p(u,i,f,o,a)):c&&(g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)),n}}function p(t,n,e,r,i){for(let o=0,a=r*i;o{if(!((o-=a)>=i))return;let u=t*r[i];const c=a*t;for(let t=i,n=i+c;t{if(!((a-=u)>=o))return;let c=n*i[o];const f=u*n,s=f+u;for(let t=o,n=o+f;t=n&&++e;else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(i=+i)>=i&&++e}return e}function _(t){return 0|t.length}function b(t){return!(t>0)}function m(t){return"object"!=typeof t||"length"in t?t:Array.from(t)}function x(t,n){let e,r=0,i=0,o=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(e=n-i,i+=e/++r,o+=e*(n-i));else{let a=-1;for(let u of t)null!=(u=n(u,++a,t))&&(u=+u)>=u&&(e=u-i,i+=e/++r,o+=e*(u-i))}if(r>1)return o/(r-1)}function w(t,n){const e=x(t,n);return e?Math.sqrt(e):e}function M(t,n){let e,r;if(void 0===n)for(const n of t)null!=n&&(void 0===e?n>=n&&(e=r=n):(e>n&&(e=n),r=o&&(e=r=o):(e>o&&(e=o),r0){for(o=t[--i];i>0&&(n=o,e=t[--i],o=n+e,r=e-(o-n),!r););i>0&&(r<0&&t[i-1]<0||r>0&&t[i-1]>0)&&(e=2*r,n=o+e,e==n-o&&(o=n))}return o}}class InternMap extends Map{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const[n,e]of t)this.set(n,e)}get(t){return super.get(A(this,t))}has(t){return super.has(A(this,t))}set(t,n){return super.set(S(this,t),n)}delete(t){return super.delete(E(this,t))}}class InternSet extends Set{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const n of t)this.add(n)}has(t){return super.has(A(this,t))}add(t){return super.add(S(this,t))}delete(t){return super.delete(E(this,t))}}function A({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):e}function S({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):(t.set(r,e),e)}function E({_intern:t,_key:n},e){const r=n(e);return t.has(r)&&(e=t.get(r),t.delete(r)),e}function N(t){return null!==t&&"object"==typeof t?t.valueOf():t}function k(t){return t}function C(t,...n){return F(t,k,k,n)}function P(t,...n){return F(t,Array.from,k,n)}function z(t,n){for(let e=1,r=n.length;et.pop().map((([n,e])=>[...t,n,e]))));return t}function $(t,n,...e){return F(t,k,n,e)}function D(t,n,...e){return F(t,Array.from,n,e)}function R(t){if(1!==t.length)throw new Error("duplicate key");return t[0]}function F(t,n,e,r){return function t(i,o){if(o>=r.length)return e(i);const a=new InternMap,u=r[o++];let c=-1;for(const t of i){const n=u(t,++c,i),e=a.get(n);e?e.push(t):a.set(n,[t])}for(const[n,e]of a)a.set(n,t(e,o));return n(a)}(t,0)}function q(t,n){return Array.from(n,(n=>t[n]))}function U(t,...n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");t=Array.from(t);let[e]=n;if(e&&2!==e.length||n.length>1){const r=Uint32Array.from(t,((t,n)=>n));return n.length>1?(n=n.map((n=>t.map(n))),r.sort(((t,e)=>{for(const r of n){const n=O(r[t],r[e]);if(n)return n}}))):(e=t.map(e),r.sort(((t,n)=>O(e[t],e[n])))),q(t,r)}return t.sort(I(e))}function I(t=n){if(t===n)return O;if("function"!=typeof t)throw new TypeError("compare is not a function");return(n,e)=>{const r=t(n,e);return r||0===r?r:(0===t(e,e))-(0===t(n,n))}}function O(t,n){return(null==t||!(t>=t))-(null==n||!(n>=n))||(tn?1:0)}var B=Array.prototype.slice;function Y(t){return()=>t}const L=Math.sqrt(50),j=Math.sqrt(10),H=Math.sqrt(2);function X(t,n,e){const r=(n-t)/Math.max(0,e),i=Math.floor(Math.log10(r)),o=r/Math.pow(10,i),a=o>=L?10:o>=j?5:o>=H?2:1;let u,c,f;return i<0?(f=Math.pow(10,-i)/a,u=Math.round(t*f),c=Math.round(n*f),u/fn&&--c,f=-f):(f=Math.pow(10,i)*a,u=Math.round(t/f),c=Math.round(n/f),u*fn&&--c),c0))return[];if((t=+t)===(n=+n))return[t];const r=n=i))return[];const u=o-i+1,c=new Array(u);if(r)if(a<0)for(let t=0;t0?(t=Math.floor(t/i)*i,n=Math.ceil(n/i)*i):i<0&&(t=Math.ceil(t*i)/i,n=Math.floor(n*i)/i),r=i}}function K(t){return Math.max(1,Math.ceil(Math.log(v(t))/Math.LN2)+1)}function Q(){var t=k,n=M,e=K;function r(r){Array.isArray(r)||(r=Array.from(r));var i,o,a,u=r.length,c=new Array(u);for(i=0;i=h)if(t>=h&&n===M){const t=V(l,h,e);isFinite(t)&&(t>0?h=(Math.floor(h/t)+1)*t:t<0&&(h=(Math.ceil(h*-t)+1)/-t))}else d.pop()}for(var p=d.length,g=0,y=p;d[g]<=l;)++g;for(;d[y-1]>h;)--y;(g||y0?d[i-1]:l,v.x1=i0)for(i=0;i=n)&&(e=n);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(e=i)&&(e=i)}return e}function tt(t,n){let e,r=-1,i=-1;if(void 0===n)for(const n of t)++i,null!=n&&(e=n)&&(e=n,r=i);else for(let o of t)null!=(o=n(o,++i,t))&&(e=o)&&(e=o,r=i);return r}function nt(t,n){let e;if(void 0===n)for(const n of t)null!=n&&(e>n||void 0===e&&n>=n)&&(e=n);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(e>i||void 0===e&&i>=i)&&(e=i)}return e}function et(t,n){let e,r=-1,i=-1;if(void 0===n)for(const n of t)++i,null!=n&&(e>n||void 0===e&&n>=n)&&(e=n,r=i);else for(let o of t)null!=(o=n(o,++i,t))&&(e>o||void 0===e&&o>=o)&&(e=o,r=i);return r}function rt(t,n,e=0,r=1/0,i){if(n=Math.floor(n),e=Math.floor(Math.max(0,e)),r=Math.floor(Math.min(t.length-1,r)),!(e<=n&&n<=r))return t;for(i=void 0===i?O:I(i);r>e;){if(r-e>600){const o=r-e+1,a=n-e+1,u=Math.log(o),c=.5*Math.exp(2*u/3),f=.5*Math.sqrt(u*c*(o-c)/o)*(a-o/2<0?-1:1);rt(t,n,Math.max(e,Math.floor(n-a*c/o+f)),Math.min(r,Math.floor(n+(o-a)*c/o+f)),i)}const o=t[n];let a=e,u=r;for(it(t,e,n),i(t[r],o)>0&&it(t,e,r);a0;)--u}0===i(t[e],o)?it(t,e,u):(++u,it(t,u,r)),u<=n&&(e=u+1),n<=u&&(r=u-1)}return t}function it(t,n,e){const r=t[n];t[n]=t[e],t[e]=r}function ot(t,e=n){let r,i=!1;if(1===e.length){let o;for(const a of t){const t=e(a);(i?n(t,o)>0:0===n(t,t))&&(r=a,o=t,i=!0)}}else for(const n of t)(i?e(n,r)>0:0===e(n,n))&&(r=n,i=!0);return r}function at(t,n,e){if(t=Float64Array.from(function*(t,n){if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(yield n);else{let e=-1;for(let r of t)null!=(r=n(r,++e,t))&&(r=+r)>=r&&(yield r)}}(t,e)),(r=t.length)&&!isNaN(n=+n)){if(n<=0||r<2)return nt(t);if(n>=1)return J(t);var r,i=(r-1)*n,o=Math.floor(i),a=J(rt(t,o).subarray(0,o+1));return a+(nt(t.subarray(o+1))-a)*(i-o)}}function ut(t,n,e=o){if((r=t.length)&&!isNaN(n=+n)){if(n<=0||r<2)return+e(t[0],0,t);if(n>=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,a=Math.floor(i),u=+e(t[a],a,t);return u+(+e(t[a+1],a+1,t)-u)*(i-a)}}function ct(t,n,e=o){if(!isNaN(n=+n)){if(r=Float64Array.from(t,((n,r)=>o(e(t[r],r,t)))),n<=0)return et(r);if(n>=1)return tt(r);var r,i=Uint32Array.from(t,((t,n)=>n)),a=r.length-1,u=Math.floor(a*n);return rt(i,u,0,a,((t,n)=>O(r[t],r[n]))),(u=ot(i.subarray(0,u+1),(t=>r[t])))>=0?u:-1}}function ft(t){return Array.from(function*(t){for(const n of t)yield*n}(t))}function st(t,n){return[t,n]}function lt(t,n,e){t=+t,n=+n,e=(i=arguments.length)<2?(n=t,t=0,1):i<3?1:+e;for(var r=-1,i=0|Math.max(0,Math.ceil((n-t)/e)),o=new Array(i);++r+t(n)}function kt(t,n){return n=Math.max(0,t.bandwidth()-2*n)/2,t.round()&&(n=Math.round(n)),e=>+t(e)+n}function Ct(){return!this.__axis}function Pt(t,n){var e=[],r=null,i=null,o=6,a=6,u=3,c="undefined"!=typeof window&&window.devicePixelRatio>1?0:.5,f=t===xt||t===Tt?-1:1,s=t===Tt||t===wt?"x":"y",l=t===xt||t===Mt?St:Et;function h(h){var d=null==r?n.ticks?n.ticks.apply(n,e):n.domain():r,p=null==i?n.tickFormat?n.tickFormat.apply(n,e):mt:i,g=Math.max(o,0)+u,y=n.range(),v=+y[0]+c,_=+y[y.length-1]+c,b=(n.bandwidth?kt:Nt)(n.copy(),c),m=h.selection?h.selection():h,x=m.selectAll(".domain").data([null]),w=m.selectAll(".tick").data(d,n).order(),M=w.exit(),T=w.enter().append("g").attr("class","tick"),A=w.select("line"),S=w.select("text");x=x.merge(x.enter().insert("path",".tick").attr("class","domain").attr("stroke","currentColor")),w=w.merge(T),A=A.merge(T.append("line").attr("stroke","currentColor").attr(s+"2",f*o)),S=S.merge(T.append("text").attr("fill","currentColor").attr(s,f*g).attr("dy",t===xt?"0em":t===Mt?"0.71em":"0.32em")),h!==m&&(x=x.transition(h),w=w.transition(h),A=A.transition(h),S=S.transition(h),M=M.transition(h).attr("opacity",At).attr("transform",(function(t){return isFinite(t=b(t))?l(t+c):this.getAttribute("transform")})),T.attr("opacity",At).attr("transform",(function(t){var n=this.parentNode.__axis;return l((n&&isFinite(n=n(t))?n:b(t))+c)}))),M.remove(),x.attr("d",t===Tt||t===wt?a?"M"+f*a+","+v+"H"+c+"V"+_+"H"+f*a:"M"+c+","+v+"V"+_:a?"M"+v+","+f*a+"V"+c+"H"+_+"V"+f*a:"M"+v+","+c+"H"+_),w.attr("opacity",1).attr("transform",(function(t){return l(b(t)+c)})),A.attr(s+"2",f*o),S.attr(s,f*g).text(p),m.filter(Ct).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",t===wt?"start":t===Tt?"end":"middle"),m.each((function(){this.__axis=b}))}return h.scale=function(t){return arguments.length?(n=t,h):n},h.ticks=function(){return e=Array.from(arguments),h},h.tickArguments=function(t){return arguments.length?(e=null==t?[]:Array.from(t),h):e.slice()},h.tickValues=function(t){return arguments.length?(r=null==t?null:Array.from(t),h):r&&r.slice()},h.tickFormat=function(t){return arguments.length?(i=t,h):i},h.tickSize=function(t){return arguments.length?(o=a=+t,h):o},h.tickSizeInner=function(t){return arguments.length?(o=+t,h):o},h.tickSizeOuter=function(t){return arguments.length?(a=+t,h):a},h.tickPadding=function(t){return arguments.length?(u=+t,h):u},h.offset=function(t){return arguments.length?(c=+t,h):c},h}var zt={value:()=>{}};function $t(){for(var t,n=0,e=arguments.length,r={};n=0&&(n=t.slice(e+1),t=t.slice(0,e)),t&&!r.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))),a=-1,u=o.length;if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++a0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Ut.hasOwnProperty(n)?{space:Ut[n],local:t}:t}function Ot(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===qt&&n.documentElement.namespaceURI===qt?n.createElement(t):n.createElementNS(e,t)}}function Bt(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function Yt(t){var n=It(t);return(n.local?Bt:Ot)(n)}function Lt(){}function jt(t){return null==t?Lt:function(){return this.querySelector(t)}}function Ht(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function Xt(){return[]}function Gt(t){return null==t?Xt:function(){return this.querySelectorAll(t)}}function Vt(t){return function(){return this.matches(t)}}function Wt(t){return function(n){return n.matches(t)}}var Zt=Array.prototype.find;function Kt(){return this.firstElementChild}var Qt=Array.prototype.filter;function Jt(){return Array.from(this.children)}function tn(t){return new Array(t.length)}function nn(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function en(t,n,e,r,i,o){for(var a,u=0,c=n.length,f=o.length;un?1:t>=n?0:NaN}function cn(t){return function(){this.removeAttribute(t)}}function fn(t){return function(){this.removeAttributeNS(t.space,t.local)}}function sn(t,n){return function(){this.setAttribute(t,n)}}function ln(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function hn(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function dn(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function pn(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function gn(t){return function(){this.style.removeProperty(t)}}function yn(t,n,e){return function(){this.style.setProperty(t,n,e)}}function vn(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function _n(t,n){return t.style.getPropertyValue(n)||pn(t).getComputedStyle(t,null).getPropertyValue(n)}function bn(t){return function(){delete this[t]}}function mn(t,n){return function(){this[t]=n}}function xn(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function wn(t){return t.trim().split(/^|\s+/)}function Mn(t){return t.classList||new Tn(t)}function Tn(t){this._node=t,this._names=wn(t.getAttribute("class")||"")}function An(t,n){for(var e=Mn(t),r=-1,i=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var Gn=[null];function Vn(t,n){this._groups=t,this._parents=n}function Wn(){return new Vn([[document.documentElement]],Gn)}function Zn(t){return"string"==typeof t?new Vn([[document.querySelector(t)]],[document.documentElement]):new Vn([[t]],Gn)}Vn.prototype=Wn.prototype={constructor:Vn,select:function(t){"function"!=typeof t&&(t=jt(t));for(var n=this._groups,e=n.length,r=new Array(e),i=0;i=m&&(m=b+1);!(_=y[m])&&++m=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=un);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?gn:"function"==typeof n?vn:yn)(t,n,null==e?"":e)):_n(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?bn:"function"==typeof n?xn:mn)(t,n)):this.node()[t]},classed:function(t,n){var e=wn(t+"");if(arguments.length<2){for(var r=Mn(this.node()),i=-1,o=e.length;++i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}}))}(t+""),a=o.length;if(!(arguments.length<2)){for(u=n?Ln:Yn,r=0;r()=>t;function fe(t,{sourceEvent:n,subject:e,target:r,identifier:i,active:o,x:a,y:u,dx:c,dy:f,dispatch:s}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},subject:{value:e,enumerable:!0,configurable:!0},target:{value:r,enumerable:!0,configurable:!0},identifier:{value:i,enumerable:!0,configurable:!0},active:{value:o,enumerable:!0,configurable:!0},x:{value:a,enumerable:!0,configurable:!0},y:{value:u,enumerable:!0,configurable:!0},dx:{value:c,enumerable:!0,configurable:!0},dy:{value:f,enumerable:!0,configurable:!0},_:{value:s}})}function se(t){return!t.ctrlKey&&!t.button}function le(){return this.parentNode}function he(t,n){return null==n?{x:t.x,y:t.y}:n}function de(){return navigator.maxTouchPoints||"ontouchstart"in this}function pe(t,n,e){t.prototype=n.prototype=e,e.constructor=t}function ge(t,n){var e=Object.create(t.prototype);for(var r in n)e[r]=n[r];return e}function ye(){}fe.prototype.on=function(){var t=this._.on.apply(this._,arguments);return t===this._?this:t};var ve=.7,_e=1/ve,be="\\s*([+-]?\\d+)\\s*",me="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",xe="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",we=/^#([0-9a-f]{3,8})$/,Me=new RegExp(`^rgb\\(${be},${be},${be}\\)$`),Te=new RegExp(`^rgb\\(${xe},${xe},${xe}\\)$`),Ae=new RegExp(`^rgba\\(${be},${be},${be},${me}\\)$`),Se=new RegExp(`^rgba\\(${xe},${xe},${xe},${me}\\)$`),Ee=new RegExp(`^hsl\\(${me},${xe},${xe}\\)$`),Ne=new RegExp(`^hsla\\(${me},${xe},${xe},${me}\\)$`),ke={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};function Ce(){return this.rgb().formatHex()}function Pe(){return this.rgb().formatRgb()}function ze(t){var n,e;return t=(t+"").trim().toLowerCase(),(n=we.exec(t))?(e=n[1].length,n=parseInt(n[1],16),6===e?$e(n):3===e?new qe(n>>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):8===e?De(n>>24&255,n>>16&255,n>>8&255,(255&n)/255):4===e?De(n>>12&15|n>>8&240,n>>8&15|n>>4&240,n>>4&15|240&n,((15&n)<<4|15&n)/255):null):(n=Me.exec(t))?new qe(n[1],n[2],n[3],1):(n=Te.exec(t))?new qe(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Ae.exec(t))?De(n[1],n[2],n[3],n[4]):(n=Se.exec(t))?De(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Ee.exec(t))?Le(n[1],n[2]/100,n[3]/100,1):(n=Ne.exec(t))?Le(n[1],n[2]/100,n[3]/100,n[4]):ke.hasOwnProperty(t)?$e(ke[t]):"transparent"===t?new qe(NaN,NaN,NaN,0):null}function $e(t){return new qe(t>>16&255,t>>8&255,255&t,1)}function De(t,n,e,r){return r<=0&&(t=n=e=NaN),new qe(t,n,e,r)}function Re(t){return t instanceof ye||(t=ze(t)),t?new qe((t=t.rgb()).r,t.g,t.b,t.opacity):new qe}function Fe(t,n,e,r){return 1===arguments.length?Re(t):new qe(t,n,e,null==r?1:r)}function qe(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function Ue(){return`#${Ye(this.r)}${Ye(this.g)}${Ye(this.b)}`}function Ie(){const t=Oe(this.opacity);return`${1===t?"rgb(":"rgba("}${Be(this.r)}, ${Be(this.g)}, ${Be(this.b)}${1===t?")":`, ${t})`}`}function Oe(t){return isNaN(t)?1:Math.max(0,Math.min(1,t))}function Be(t){return Math.max(0,Math.min(255,Math.round(t)||0))}function Ye(t){return((t=Be(t))<16?"0":"")+t.toString(16)}function Le(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Xe(t,n,e,r)}function je(t){if(t instanceof Xe)return new Xe(t.h,t.s,t.l,t.opacity);if(t instanceof ye||(t=ze(t)),!t)return new Xe;if(t instanceof Xe)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,c=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&c<1?0:a,new Xe(a,u,c,t.opacity)}function He(t,n,e,r){return 1===arguments.length?je(t):new Xe(t,n,e,null==r?1:r)}function Xe(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Ge(t){return(t=(t||0)%360)<0?t+360:t}function Ve(t){return Math.max(0,Math.min(1,t||0))}function We(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}pe(ye,ze,{copy(t){return Object.assign(new this.constructor,this,t)},displayable(){return this.rgb().displayable()},hex:Ce,formatHex:Ce,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return je(this).formatHsl()},formatRgb:Pe,toString:Pe}),pe(qe,Fe,ge(ye,{brighter(t){return t=null==t?_e:Math.pow(_e,t),new qe(this.r*t,this.g*t,this.b*t,this.opacity)},darker(t){return t=null==t?ve:Math.pow(ve,t),new qe(this.r*t,this.g*t,this.b*t,this.opacity)},rgb(){return this},clamp(){return new qe(Be(this.r),Be(this.g),Be(this.b),Oe(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:Ue,formatHex:Ue,formatHex8:function(){return`#${Ye(this.r)}${Ye(this.g)}${Ye(this.b)}${Ye(255*(isNaN(this.opacity)?1:this.opacity))}`},formatRgb:Ie,toString:Ie})),pe(Xe,He,ge(ye,{brighter(t){return t=null==t?_e:Math.pow(_e,t),new Xe(this.h,this.s,this.l*t,this.opacity)},darker(t){return t=null==t?ve:Math.pow(ve,t),new Xe(this.h,this.s,this.l*t,this.opacity)},rgb(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new qe(We(t>=240?t-240:t+120,i,r),We(t,i,r),We(t<120?t+240:t-120,i,r),this.opacity)},clamp(){return new Xe(Ge(this.h),Ve(this.s),Ve(this.l),Oe(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const t=Oe(this.opacity);return`${1===t?"hsl(":"hsla("}${Ge(this.h)}, ${100*Ve(this.s)}%, ${100*Ve(this.l)}%${1===t?")":`, ${t})`}`}}));const Ze=Math.PI/180,Ke=180/Math.PI,Qe=.96422,Je=1,tr=.82521,nr=4/29,er=6/29,rr=3*er*er,ir=er*er*er;function or(t){if(t instanceof ur)return new ur(t.l,t.a,t.b,t.opacity);if(t instanceof pr)return gr(t);t instanceof qe||(t=Re(t));var n,e,r=lr(t.r),i=lr(t.g),o=lr(t.b),a=cr((.2225045*r+.7168786*i+.0606169*o)/Je);return r===i&&i===o?n=e=a:(n=cr((.4360747*r+.3850649*i+.1430804*o)/Qe),e=cr((.0139322*r+.0971045*i+.7141733*o)/tr)),new ur(116*a-16,500*(n-a),200*(a-e),t.opacity)}function ar(t,n,e,r){return 1===arguments.length?or(t):new ur(t,n,e,null==r?1:r)}function ur(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function cr(t){return t>ir?Math.pow(t,1/3):t/rr+nr}function fr(t){return t>er?t*t*t:rr*(t-nr)}function sr(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function lr(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function hr(t){if(t instanceof pr)return new pr(t.h,t.c,t.l,t.opacity);if(t instanceof ur||(t=or(t)),0===t.a&&0===t.b)return new pr(NaN,0=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],a=r>0?t[r-1]:2*i-o,u=r()=>t;function Cr(t,n){return function(e){return t+e*n}}function Pr(t,n){var e=n-t;return e?Cr(t,e>180||e<-180?e-360*Math.round(e/360):e):kr(isNaN(t)?n:t)}function zr(t){return 1==(t=+t)?$r:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):kr(isNaN(n)?e:n)}}function $r(t,n){var e=n-t;return e?Cr(t,e):kr(isNaN(t)?n:t)}var Dr=function t(n){var e=zr(n);function r(t,n){var r=e((t=Fe(t)).r,(n=Fe(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),a=$r(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}return r.gamma=t,r}(1);function Rr(t){return function(n){var e,r,i=n.length,o=new Array(i),a=new Array(i),u=new Array(i);for(e=0;eo&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,c.push({i:a,x:Yr(e,r)})),o=Hr.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:Yr(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,a.rotate,u,c),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:Yr(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,a.skewX,u,c),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:Yr(t,e)},{i:u-2,x:Yr(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,c),o=a=null,function(t){for(var n,e=-1,r=c.length;++e=0&&n._call.call(void 0,t),n=n._next;--yi}function Ci(){xi=(mi=Mi.now())+wi,yi=vi=0;try{ki()}finally{yi=0,function(){var t,n,e=pi,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:pi=n);gi=t,zi(r)}(),xi=0}}function Pi(){var t=Mi.now(),n=t-mi;n>bi&&(wi-=n,mi=t)}function zi(t){yi||(vi&&(vi=clearTimeout(vi)),t-xi>24?(t<1/0&&(vi=setTimeout(Ci,t-Mi.now()-wi)),_i&&(_i=clearInterval(_i))):(_i||(mi=Mi.now(),_i=setInterval(Pi,bi)),yi=1,Ti(Ci)))}function $i(t,n,e){var r=new Ei;return n=null==n?0:+n,r.restart((e=>{r.stop(),t(e+n)}),n,e),r}Ei.prototype=Ni.prototype={constructor:Ei,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?Ai():+e)+(null==n?0:+n),this._next||gi===this||(gi?gi._next=this:pi=this,gi=this),this._call=t,this._time=e,zi()},stop:function(){this._call&&(this._call=null,this._time=1/0,zi())}};var Di=$t("start","end","cancel","interrupt"),Ri=[],Fi=0,qi=1,Ui=2,Ii=3,Oi=4,Bi=5,Yi=6;function Li(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(t){e.state=qi,e.timer.restart(a,e.delay,e.time),e.delay<=t&&a(t-e.delay)}function a(o){var f,s,l,h;if(e.state!==qi)return c();for(f in i)if((h=i[f]).name===e.name){if(h.state===Ii)return $i(a);h.state===Oi?(h.state=Yi,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete i[f]):+fFi)throw new Error("too late; already scheduled");return e}function Hi(t,n){var e=Xi(t,n);if(e.state>Ii)throw new Error("too late; already running");return e}function Xi(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function Gi(t,n){var e,r,i,o=t.__transition,a=!0;if(o){for(i in n=null==n?null:n+"",o)(e=o[i]).name===n?(r=e.state>Ui&&e.state=0&&(t=t.slice(0,n)),!t||"start"===t}))}(n)?ji:Hi;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i}}(e,t,n))},attr:function(t,n){var e=It(t),r="transform"===e?ni:Ki;return this.attrTween(t,"function"==typeof n?(e.local?ro:eo)(e,r,Zi(this,"attr."+t,n)):null==n?(e.local?Ji:Qi)(e):(e.local?no:to)(e,r,n))},attrTween:function(t,n){var e="attr."+t;if(arguments.length<2)return(e=this.tween(e))&&e._value;if(null==n)return this.tween(e,null);if("function"!=typeof n)throw new Error;var r=It(t);return this.tween(e,(r.local?io:oo)(r,n))},style:function(t,n,e){var r="transform"==(t+="")?ti:Ki;return null==n?this.styleTween(t,function(t,n){var e,r,i;return function(){var o=_n(this,t),a=(this.style.removeProperty(t),_n(this,t));return o===a?null:o===e&&a===r?i:i=n(e=o,r=a)}}(t,r)).on("end.style."+t,lo(t)):"function"==typeof n?this.styleTween(t,function(t,n,e){var r,i,o;return function(){var a=_n(this,t),u=e(this),c=u+"";return null==u&&(this.style.removeProperty(t),c=u=_n(this,t)),a===c?null:a===r&&c===i?o:(i=c,o=n(r=a,u))}}(t,r,Zi(this,"style."+t,n))).each(function(t,n){var e,r,i,o,a="style."+n,u="end."+a;return function(){var c=Hi(this,t),f=c.on,s=null==c.value[a]?o||(o=lo(n)):void 0;f===e&&i===s||(r=(e=f).copy()).on(u,i=s),c.on=r}}(this._id,t)):this.styleTween(t,function(t,n,e){var r,i,o=e+"";return function(){var a=_n(this,t);return a===o?null:a===r?i:i=n(r=a,e)}}(t,r,n),e).on("end.style."+t,null)},styleTween:function(t,n,e){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==n)return this.tween(r,null);if("function"!=typeof n)throw new Error;return this.tween(r,function(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&function(t,n,e){return function(r){this.style.setProperty(t,n.call(this,r),e)}}(t,o,e)),r}return o._value=n,o}(t,n,null==e?"":e))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var n=t(this);this.textContent=null==n?"":n}}(Zi(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},textTween:function(t){var n="text";if(arguments.length<1)return(n=this.tween(n))&&n._value;if(null==t)return this.tween(n,null);if("function"!=typeof t)throw new Error;return this.tween(n,function(t){var n,e;function r(){var r=t.apply(this,arguments);return r!==e&&(n=(e=r)&&function(t){return function(n){this.textContent=t.call(this,n)}}(r)),n}return r._value=t,r}(t))},remove:function(){return this.on("end.remove",function(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}(this._id))},tween:function(t,n){var e=this._id;if(t+="",arguments.length<2){for(var r,i=Xi(this.node(),e).tween,o=0,a=i.length;o()=>t;function Qo(t,{sourceEvent:n,target:e,selection:r,mode:i,dispatch:o}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},target:{value:e,enumerable:!0,configurable:!0},selection:{value:r,enumerable:!0,configurable:!0},mode:{value:i,enumerable:!0,configurable:!0},_:{value:o}})}function Jo(t){t.preventDefault(),t.stopImmediatePropagation()}var ta={name:"drag"},na={name:"space"},ea={name:"handle"},ra={name:"center"};const{abs:ia,max:oa,min:aa}=Math;function ua(t){return[+t[0],+t[1]]}function ca(t){return[ua(t[0]),ua(t[1])]}var fa={name:"x",handles:["w","e"].map(va),input:function(t,n){return null==t?null:[[+t[0],n[0][1]],[+t[1],n[1][1]]]},output:function(t){return t&&[t[0][0],t[1][0]]}},sa={name:"y",handles:["n","s"].map(va),input:function(t,n){return null==t?null:[[n[0][0],+t[0]],[n[1][0],+t[1]]]},output:function(t){return t&&[t[0][1],t[1][1]]}},la={name:"xy",handles:["n","w","e","s","nw","ne","sw","se"].map(va),input:function(t){return null==t?null:ca(t)},output:function(t){return t}},ha={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},da={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"},pa={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"},ga={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1},ya={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1};function va(t){return{type:t}}function _a(t){return!t.ctrlKey&&!t.button}function ba(){var t=this.ownerSVGElement||this;return t.hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]}function ma(){return navigator.maxTouchPoints||"ontouchstart"in this}function xa(t){for(;!t.__brush;)if(!(t=t.parentNode))return;return t.__brush}function wa(t){var n,e=ba,r=_a,i=ma,o=!0,a=$t("start","brush","end"),u=6;function c(n){var e=n.property("__brush",g).selectAll(".overlay").data([va("overlay")]);e.enter().append("rect").attr("class","overlay").attr("pointer-events","all").attr("cursor",ha.overlay).merge(e).each((function(){var t=xa(this).extent;Zn(this).attr("x",t[0][0]).attr("y",t[0][1]).attr("width",t[1][0]-t[0][0]).attr("height",t[1][1]-t[0][1])})),n.selectAll(".selection").data([va("selection")]).enter().append("rect").attr("class","selection").attr("cursor",ha.selection).attr("fill","#777").attr("fill-opacity",.3).attr("stroke","#fff").attr("shape-rendering","crispEdges");var r=n.selectAll(".handle").data(t.handles,(function(t){return t.type}));r.exit().remove(),r.enter().append("rect").attr("class",(function(t){return"handle handle--"+t.type})).attr("cursor",(function(t){return ha[t.type]})),n.each(f).attr("fill","none").attr("pointer-events","all").on("mousedown.brush",h).filter(i).on("touchstart.brush",h).on("touchmove.brush",d).on("touchend.brush touchcancel.brush",p).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function f(){var t=Zn(this),n=xa(this).selection;n?(t.selectAll(".selection").style("display",null).attr("x",n[0][0]).attr("y",n[0][1]).attr("width",n[1][0]-n[0][0]).attr("height",n[1][1]-n[0][1]),t.selectAll(".handle").style("display",null).attr("x",(function(t){return"e"===t.type[t.type.length-1]?n[1][0]-u/2:n[0][0]-u/2})).attr("y",(function(t){return"s"===t.type[0]?n[1][1]-u/2:n[0][1]-u/2})).attr("width",(function(t){return"n"===t.type||"s"===t.type?n[1][0]-n[0][0]+u:u})).attr("height",(function(t){return"e"===t.type||"w"===t.type?n[1][1]-n[0][1]+u:u}))):t.selectAll(".selection,.handle").style("display","none").attr("x",null).attr("y",null).attr("width",null).attr("height",null)}function s(t,n,e){var r=t.__brush.emitter;return!r||e&&r.clean?new l(t,n,e):r}function l(t,n,e){this.that=t,this.args=n,this.state=t.__brush,this.active=0,this.clean=e}function h(e){if((!n||e.touches)&&r.apply(this,arguments)){var i,a,u,c,l,h,d,p,g,y,v,_=this,b=e.target.__data__.type,m="selection"===(o&&e.metaKey?b="overlay":b)?ta:o&&e.altKey?ra:ea,x=t===sa?null:ga[b],w=t===fa?null:ya[b],M=xa(_),T=M.extent,A=M.selection,S=T[0][0],E=T[0][1],N=T[1][0],k=T[1][1],C=0,P=0,z=x&&w&&o&&e.shiftKey,$=Array.from(e.touches||[e],(t=>{const n=t.identifier;return(t=ne(t,_)).point0=t.slice(),t.identifier=n,t}));Gi(_);var D=s(_,arguments,!0).beforestart();if("overlay"===b){A&&(g=!0);const n=[$[0],$[1]||$[0]];M.selection=A=[[i=t===sa?S:aa(n[0][0],n[1][0]),u=t===fa?E:aa(n[0][1],n[1][1])],[l=t===sa?N:oa(n[0][0],n[1][0]),d=t===fa?k:oa(n[0][1],n[1][1])]],$.length>1&&I(e)}else i=A[0][0],u=A[0][1],l=A[1][0],d=A[1][1];a=i,c=u,h=l,p=d;var R=Zn(_).attr("pointer-events","none"),F=R.selectAll(".overlay").attr("cursor",ha[b]);if(e.touches)D.moved=U,D.ended=O;else{var q=Zn(e.view).on("mousemove.brush",U,!0).on("mouseup.brush",O,!0);o&&q.on("keydown.brush",(function(t){switch(t.keyCode){case 16:z=x&&w;break;case 18:m===ea&&(x&&(l=h-C*x,i=a+C*x),w&&(d=p-P*w,u=c+P*w),m=ra,I(t));break;case 32:m!==ea&&m!==ra||(x<0?l=h-C:x>0&&(i=a-C),w<0?d=p-P:w>0&&(u=c-P),m=na,F.attr("cursor",ha.selection),I(t));break;default:return}Jo(t)}),!0).on("keyup.brush",(function(t){switch(t.keyCode){case 16:z&&(y=v=z=!1,I(t));break;case 18:m===ra&&(x<0?l=h:x>0&&(i=a),w<0?d=p:w>0&&(u=c),m=ea,I(t));break;case 32:m===na&&(t.altKey?(x&&(l=h-C*x,i=a+C*x),w&&(d=p-P*w,u=c+P*w),m=ra):(x<0?l=h:x>0&&(i=a),w<0?d=p:w>0&&(u=c),m=ea),F.attr("cursor",ha[b]),I(t));break;default:return}Jo(t)}),!0),ae(e.view)}f.call(_),D.start(e,m.name)}function U(t){for(const n of t.changedTouches||[t])for(const t of $)t.identifier===n.identifier&&(t.cur=ne(n,_));if(z&&!y&&!v&&1===$.length){const t=$[0];ia(t.cur[0]-t[0])>ia(t.cur[1]-t[1])?v=!0:y=!0}for(const t of $)t.cur&&(t[0]=t.cur[0],t[1]=t.cur[1]);g=!0,Jo(t),I(t)}function I(t){const n=$[0],e=n.point0;var r;switch(C=n[0]-e[0],P=n[1]-e[1],m){case na:case ta:x&&(C=oa(S-i,aa(N-l,C)),a=i+C,h=l+C),w&&(P=oa(E-u,aa(k-d,P)),c=u+P,p=d+P);break;case ea:$[1]?(x&&(a=oa(S,aa(N,$[0][0])),h=oa(S,aa(N,$[1][0])),x=1),w&&(c=oa(E,aa(k,$[0][1])),p=oa(E,aa(k,$[1][1])),w=1)):(x<0?(C=oa(S-i,aa(N-i,C)),a=i+C,h=l):x>0&&(C=oa(S-l,aa(N-l,C)),a=i,h=l+C),w<0?(P=oa(E-u,aa(k-u,P)),c=u+P,p=d):w>0&&(P=oa(E-d,aa(k-d,P)),c=u,p=d+P));break;case ra:x&&(a=oa(S,aa(N,i-C*x)),h=oa(S,aa(N,l+C*x))),w&&(c=oa(E,aa(k,u-P*w)),p=oa(E,aa(k,d+P*w)))}ht+e))}function za(t,n){var e=0,r=null,i=null,o=null;function a(a){var u,c=a.length,f=new Array(c),s=Pa(0,c),l=new Array(c*c),h=new Array(c),d=0;a=Float64Array.from({length:c*c},n?(t,n)=>a[n%c][n/c|0]:(t,n)=>a[n/c|0][n%c]);for(let n=0;nr(f[t],f[n])));for(const e of s){const r=n;if(t){const t=Pa(1+~c,c).filter((t=>t<0?a[~t*c+e]:a[e*c+t]));i&&t.sort(((t,n)=>i(t<0?-a[~t*c+e]:a[e*c+t],n<0?-a[~n*c+e]:a[e*c+n])));for(const r of t)if(r<0){(l[~r*c+e]||(l[~r*c+e]={source:null,target:null})).target={index:e,startAngle:n,endAngle:n+=a[~r*c+e]*d,value:a[~r*c+e]}}else{(l[e*c+r]||(l[e*c+r]={source:null,target:null})).source={index:e,startAngle:n,endAngle:n+=a[e*c+r]*d,value:a[e*c+r]}}h[e]={index:e,startAngle:r,endAngle:n,value:f[e]}}else{const t=Pa(0,c).filter((t=>a[e*c+t]||a[t*c+e]));i&&t.sort(((t,n)=>i(a[e*c+t],a[e*c+n])));for(const r of t){let t;if(e=0))throw new Error(`invalid digits: ${t}`);if(n>15)return qa;const e=10**n;return function(t){this._+=t[0];for(let n=1,r=t.length;nRa)if(Math.abs(s*u-c*f)>Ra&&i){let h=e-o,d=r-a,p=u*u+c*c,g=h*h+d*d,y=Math.sqrt(p),v=Math.sqrt(l),_=i*Math.tan(($a-Math.acos((p+l-g)/(2*y*v)))/2),b=_/v,m=_/y;Math.abs(b-1)>Ra&&this._append`L${t+b*f},${n+b*s}`,this._append`A${i},${i},0,0,${+(s*h>f*d)},${this._x1=t+m*u},${this._y1=n+m*c}`}else this._append`L${this._x1=t},${this._y1=n}`;else;}arc(t,n,e,r,i,o){if(t=+t,n=+n,o=!!o,(e=+e)<0)throw new Error(`negative radius: ${e}`);let a=e*Math.cos(r),u=e*Math.sin(r),c=t+a,f=n+u,s=1^o,l=o?r-i:i-r;null===this._x1?this._append`M${c},${f}`:(Math.abs(this._x1-c)>Ra||Math.abs(this._y1-f)>Ra)&&this._append`L${c},${f}`,e&&(l<0&&(l=l%Da+Da),l>Fa?this._append`A${e},${e},0,1,${s},${t-a},${n-u}A${e},${e},0,1,${s},${this._x1=c},${this._y1=f}`:l>Ra&&this._append`A${e},${e},0,${+(l>=$a)},${s},${this._x1=t+e*Math.cos(i)},${this._y1=n+e*Math.sin(i)}`)}rect(t,n,e,r){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}h${e=+e}v${+r}h${-e}Z`}toString(){return this._}};function Ia(){return new Ua}Ia.prototype=Ua.prototype;var Oa=Array.prototype.slice;function Ba(t){return function(){return t}}function Ya(t){return t.source}function La(t){return t.target}function ja(t){return t.radius}function Ha(t){return t.startAngle}function Xa(t){return t.endAngle}function Ga(){return 0}function Va(){return 10}function Wa(t){var n=Ya,e=La,r=ja,i=ja,o=Ha,a=Xa,u=Ga,c=null;function f(){var f,s=n.apply(this,arguments),l=e.apply(this,arguments),h=u.apply(this,arguments)/2,d=Oa.call(arguments),p=+r.apply(this,(d[0]=s,d)),g=o.apply(this,d)-Ea,y=a.apply(this,d)-Ea,v=+i.apply(this,(d[0]=l,d)),_=o.apply(this,d)-Ea,b=a.apply(this,d)-Ea;if(c||(c=f=Ia()),h>Ca&&(Ma(y-g)>2*h+Ca?y>g?(g+=h,y-=h):(g-=h,y+=h):g=y=(g+y)/2,Ma(b-_)>2*h+Ca?b>_?(_+=h,b-=h):(_-=h,b+=h):_=b=(_+b)/2),c.moveTo(p*Ta(g),p*Aa(g)),c.arc(0,0,p,g,y),g!==_||y!==b)if(t){var m=v-+t.apply(this,arguments),x=(_+b)/2;c.quadraticCurveTo(0,0,m*Ta(_),m*Aa(_)),c.lineTo(v*Ta(x),v*Aa(x)),c.lineTo(m*Ta(b),m*Aa(b))}else c.quadraticCurveTo(0,0,v*Ta(_),v*Aa(_)),c.arc(0,0,v,_,b);if(c.quadraticCurveTo(0,0,p*Ta(g),p*Aa(g)),c.closePath(),f)return c=null,f+""||null}return t&&(f.headRadius=function(n){return arguments.length?(t="function"==typeof n?n:Ba(+n),f):t}),f.radius=function(t){return arguments.length?(r=i="function"==typeof t?t:Ba(+t),f):r},f.sourceRadius=function(t){return arguments.length?(r="function"==typeof t?t:Ba(+t),f):r},f.targetRadius=function(t){return arguments.length?(i="function"==typeof t?t:Ba(+t),f):i},f.startAngle=function(t){return arguments.length?(o="function"==typeof t?t:Ba(+t),f):o},f.endAngle=function(t){return arguments.length?(a="function"==typeof t?t:Ba(+t),f):a},f.padAngle=function(t){return arguments.length?(u="function"==typeof t?t:Ba(+t),f):u},f.source=function(t){return arguments.length?(n=t,f):n},f.target=function(t){return arguments.length?(e=t,f):e},f.context=function(t){return arguments.length?(c=null==t?null:t,f):c},f}var Za=Array.prototype.slice;function Ka(t,n){return t-n}var Qa=t=>()=>t;function Ja(t,n){for(var e,r=-1,i=n.length;++rr!=d>r&&e<(h-f)*(r-s)/(d-s)+f&&(i=-i)}return i}function nu(t,n,e){var r,i,o,a;return function(t,n,e){return(n[0]-t[0])*(e[1]-t[1])==(e[0]-t[0])*(n[1]-t[1])}(t,n,e)&&(i=t[r=+(t[0]===n[0])],o=e[r],a=n[r],i<=o&&o<=a||a<=o&&o<=i)}function eu(){}var ru=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]];function iu(){var t=1,n=1,e=K,r=u;function i(t){var n=e(t);if(Array.isArray(n))n=n.slice().sort(Ka);else{const e=M(t,ou);for(n=G(...Z(e[0],e[1],n),n);n[n.length-1]>=e[1];)n.pop();for(;n[1]o(t,n)))}function o(e,i){const o=null==i?NaN:+i;if(isNaN(o))throw new Error(`invalid value: ${i}`);var u=[],c=[];return function(e,r,i){var o,u,c,f,s,l,h=new Array,d=new Array;o=u=-1,f=au(e[0],r),ru[f<<1].forEach(p);for(;++o=r,ru[s<<2].forEach(p);for(;++o0?u.push([t]):c.push(t)})),c.forEach((function(t){for(var n,e=0,r=u.length;e0&&o0&&a=0&&o>=0))throw new Error("invalid size");return t=r,n=o,i},i.thresholds=function(t){return arguments.length?(e="function"==typeof t?t:Array.isArray(t)?Qa(Za.call(t)):Qa(t),i):e},i.smooth=function(t){return arguments.length?(r=t?u:eu,i):r===u},i}function ou(t){return isFinite(t)?t:NaN}function au(t,n){return null!=t&&+t>=n}function uu(t){return null==t||isNaN(t=+t)?-1/0:t}function cu(t,n,e,r){const i=r-n,o=e-n,a=isFinite(i)||isFinite(o)?i/o:Math.sign(i)/Math.sign(o);return isNaN(a)?t:t+a-.5}function fu(t){return t[0]}function su(t){return t[1]}function lu(){return 1}const hu=134217729,du=33306690738754706e-32;function pu(t,n,e,r,i){let o,a,u,c,f=n[0],s=r[0],l=0,h=0;s>f==s>-f?(o=f,f=n[++l]):(o=s,s=r[++h]);let d=0;if(lf==s>-f?(a=f+o,u=o-(a-f),f=n[++l]):(a=s+o,u=o-(a-s),s=r[++h]),o=a,0!==u&&(i[d++]=u);lf==s>-f?(a=o+f,c=a-o,u=o-(a-c)+(f-c),f=n[++l]):(a=o+s,c=a-o,u=o-(a-c)+(s-c),s=r[++h]),o=a,0!==u&&(i[d++]=u);for(;l=33306690738754716e-32*f?c:-function(t,n,e,r,i,o,a){let u,c,f,s,l,h,d,p,g,y,v,_,b,m,x,w,M,T;const A=t-i,S=e-i,E=n-o,N=r-o;m=A*N,h=hu*A,d=h-(h-A),p=A-d,h=hu*N,g=h-(h-N),y=N-g,x=p*y-(m-d*g-p*g-d*y),w=E*S,h=hu*E,d=h-(h-E),p=E-d,h=hu*S,g=h-(h-S),y=S-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,_u[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,_u[1]=b-(v+l)+(l-w),T=_+v,l=T-_,_u[2]=_-(T-l)+(v-l),_u[3]=T;let k=function(t,n){let e=n[0];for(let r=1;r=C||-k>=C)return k;if(l=t-A,u=t-(A+l)+(l-i),l=e-S,f=e-(S+l)+(l-i),l=n-E,c=n-(E+l)+(l-o),l=r-N,s=r-(N+l)+(l-o),0===u&&0===c&&0===f&&0===s)return k;if(C=vu*a+du*Math.abs(k),k+=A*s+N*u-(E*f+S*c),k>=C||-k>=C)return k;m=u*N,h=hu*u,d=h-(h-u),p=u-d,h=hu*N,g=h-(h-N),y=N-g,x=p*y-(m-d*g-p*g-d*y),w=c*S,h=hu*c,d=h-(h-c),p=c-d,h=hu*S,g=h-(h-S),y=S-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const P=pu(4,_u,4,wu,bu);m=A*s,h=hu*A,d=h-(h-A),p=A-d,h=hu*s,g=h-(h-s),y=s-g,x=p*y-(m-d*g-p*g-d*y),w=E*f,h=hu*E,d=h-(h-E),p=E-d,h=hu*f,g=h-(h-f),y=f-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const z=pu(P,bu,4,wu,mu);m=u*s,h=hu*u,d=h-(h-u),p=u-d,h=hu*s,g=h-(h-s),y=s-g,x=p*y-(m-d*g-p*g-d*y),w=c*f,h=hu*c,d=h-(h-c),p=c-d,h=hu*f,g=h-(h-f),y=f-g,M=p*y-(w-d*g-p*g-d*y),v=x-M,l=x-v,wu[0]=x-(v+l)+(l-M),_=m+v,l=_-m,b=m-(_-l)+(v-l),v=b-w,l=b-v,wu[1]=b-(v+l)+(l-w),T=_+v,l=T-_,wu[2]=_-(T-l)+(v-l),wu[3]=T;const $=pu(z,mu,4,wu,xu);return xu[$-1]}(t,n,e,r,i,o,f)}const Tu=Math.pow(2,-52),Au=new Uint32Array(512);class Su{static from(t,n=zu,e=$u){const r=t.length,i=new Float64Array(2*r);for(let o=0;o>1;if(n>0&&"number"!=typeof t[0])throw new Error("Expected coords to contain numbers.");this.coords=t;const e=Math.max(2*n-5,0);this._triangles=new Uint32Array(3*e),this._halfedges=new Int32Array(3*e),this._hashSize=Math.ceil(Math.sqrt(n)),this._hullPrev=new Uint32Array(n),this._hullNext=new Uint32Array(n),this._hullTri=new Uint32Array(n),this._hullHash=new Int32Array(this._hashSize),this._ids=new Uint32Array(n),this._dists=new Float64Array(n),this.update()}update(){const{coords:t,_hullPrev:n,_hullNext:e,_hullTri:r,_hullHash:i}=this,o=t.length>>1;let a=1/0,u=1/0,c=-1/0,f=-1/0;for(let n=0;nc&&(c=e),r>f&&(f=r),this._ids[n]=n}const s=(a+c)/2,l=(u+f)/2;let h,d,p;for(let n=0,e=1/0;n0&&(d=n,e=r)}let v=t[2*d],_=t[2*d+1],b=1/0;for(let n=0;nr&&(n[e++]=i,r=o)}return this.hull=n.subarray(0,e),this.triangles=new Uint32Array(0),void(this.halfedges=new Uint32Array(0))}if(Mu(g,y,v,_,m,x)<0){const t=d,n=v,e=_;d=p,v=m,_=x,p=t,m=n,x=e}const w=function(t,n,e,r,i,o){const a=e-t,u=r-n,c=i-t,f=o-n,s=a*a+u*u,l=c*c+f*f,h=.5/(a*f-u*c),d=t+(f*s-u*l)*h,p=n+(a*l-c*s)*h;return{x:d,y:p}}(g,y,v,_,m,x);this._cx=w.x,this._cy=w.y;for(let n=0;n0&&Math.abs(f-o)<=Tu&&Math.abs(s-a)<=Tu)continue;if(o=f,a=s,c===h||c===d||c===p)continue;let l=0;for(let t=0,n=this._hashKey(f,s);t=0;)if(y=g,y===l){y=-1;break}if(-1===y)continue;let v=this._addTriangle(y,c,e[y],-1,-1,r[y]);r[c]=this._legalize(v+2),r[y]=v,M++;let _=e[y];for(;g=e[_],Mu(f,s,t[2*_],t[2*_+1],t[2*g],t[2*g+1])<0;)v=this._addTriangle(_,c,g,r[c],-1,r[_]),r[c]=this._legalize(v+2),e[_]=_,M--,_=g;if(y===l)for(;g=n[y],Mu(f,s,t[2*g],t[2*g+1],t[2*y],t[2*y+1])<0;)v=this._addTriangle(g,c,y,-1,r[y],r[g]),this._legalize(v+2),r[g]=v,e[y]=y,M--,y=g;this._hullStart=n[c]=y,e[y]=n[_]=c,e[c]=_,i[this._hashKey(f,s)]=c,i[this._hashKey(t[2*y],t[2*y+1])]=y}this.hull=new Uint32Array(M);for(let t=0,n=this._hullStart;t0?3-e:1+e)/4}(t-this._cx,n-this._cy)*this._hashSize)%this._hashSize}_legalize(t){const{_triangles:n,_halfedges:e,coords:r}=this;let i=0,o=0;for(;;){const a=e[t],u=t-t%3;if(o=u+(t+2)%3,-1===a){if(0===i)break;t=Au[--i];continue}const c=a-a%3,f=u+(t+1)%3,s=c+(a+2)%3,l=n[o],h=n[t],d=n[f],p=n[s];if(Nu(r[2*l],r[2*l+1],r[2*h],r[2*h+1],r[2*d],r[2*d+1],r[2*p],r[2*p+1])){n[t]=p,n[a]=l;const r=e[s];if(-1===r){let n=this._hullStart;do{if(this._hullTri[n]===s){this._hullTri[n]=t;break}n=this._hullPrev[n]}while(n!==this._hullStart)}this._link(t,r),this._link(a,e[o]),this._link(o,s);const u=c+(a+1)%3;i=e&&n[t[a]]>o;)t[a+1]=t[a--];t[a+1]=r}else{let i=e+1,o=r;Pu(t,e+r>>1,i),n[t[e]]>n[t[r]]&&Pu(t,e,r),n[t[i]]>n[t[r]]&&Pu(t,i,r),n[t[e]]>n[t[i]]&&Pu(t,e,i);const a=t[i],u=n[a];for(;;){do{i++}while(n[t[i]]u);if(o=o-e?(Cu(t,n,i,r),Cu(t,n,e,o-1)):(Cu(t,n,e,o-1),Cu(t,n,i,r))}}function Pu(t,n,e){const r=t[n];t[n]=t[e],t[e]=r}function zu(t){return t[0]}function $u(t){return t[1]}const Du=1e-6;class Ru{constructor(){this._x0=this._y0=this._x1=this._y1=null,this._=""}moveTo(t,n){this._+=`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}`}closePath(){null!==this._x1&&(this._x1=this._x0,this._y1=this._y0,this._+="Z")}lineTo(t,n){this._+=`L${this._x1=+t},${this._y1=+n}`}arc(t,n,e){const r=(t=+t)+(e=+e),i=n=+n;if(e<0)throw new Error("negative radius");null===this._x1?this._+=`M${r},${i}`:(Math.abs(this._x1-r)>Du||Math.abs(this._y1-i)>Du)&&(this._+="L"+r+","+i),e&&(this._+=`A${e},${e},0,1,1,${t-e},${n}A${e},${e},0,1,1,${this._x1=r},${this._y1=i}`)}rect(t,n,e,r){this._+=`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}h${+e}v${+r}h${-e}Z`}value(){return this._||null}}class Fu{constructor(){this._=[]}moveTo(t,n){this._.push([t,n])}closePath(){this._.push(this._[0].slice())}lineTo(t,n){this._.push([t,n])}value(){return this._.length?this._:null}}class qu{constructor(t,[n,e,r,i]=[0,0,960,500]){if(!((r=+r)>=(n=+n)&&(i=+i)>=(e=+e)))throw new Error("invalid bounds");this.delaunay=t,this._circumcenters=new Float64Array(2*t.points.length),this.vectors=new Float64Array(2*t.points.length),this.xmax=r,this.xmin=n,this.ymax=i,this.ymin=e,this._init()}update(){return this.delaunay.update(),this._init(),this}_init(){const{delaunay:{points:t,hull:n,triangles:e},vectors:r}=this;let i,o;const a=this.circumcenters=this._circumcenters.subarray(0,e.length/3*2);for(let r,u,c=0,f=0,s=e.length;c1;)i-=2;for(let t=2;t0){if(n>=this.ymax)return null;(i=(this.ymax-n)/r)0){if(t>=this.xmax)return null;(i=(this.xmax-t)/e)this.xmax?2:0)|(nthis.ymax?8:0)}_simplify(t){if(t&&t.length>4){for(let n=0;n2&&function(t){const{triangles:n,coords:e}=t;for(let t=0;t1e-10)return!1}return!0}(t)){this.collinear=Int32Array.from({length:n.length/2},((t,n)=>n)).sort(((t,e)=>n[2*t]-n[2*e]||n[2*t+1]-n[2*e+1]));const t=this.collinear[0],e=this.collinear[this.collinear.length-1],r=[n[2*t],n[2*t+1],n[2*e],n[2*e+1]],i=1e-8*Math.hypot(r[3]-r[1],r[2]-r[0]);for(let t=0,e=n.length/2;t0&&(this.triangles=new Int32Array(3).fill(-1),this.halfedges=new Int32Array(3).fill(-1),this.triangles[0]=r[0],o[r[0]]=1,2===r.length&&(o[r[1]]=0,this.triangles[1]=r[1],this.triangles[2]=r[1]))}voronoi(t){return new qu(this,t)}*neighbors(t){const{inedges:n,hull:e,_hullIndex:r,halfedges:i,triangles:o,collinear:a}=this;if(a){const n=a.indexOf(t);return n>0&&(yield a[n-1]),void(n=0&&i!==e&&i!==r;)e=i;return i}_step(t,n,e){const{inedges:r,hull:i,_hullIndex:o,halfedges:a,triangles:u,points:c}=this;if(-1===r[t]||!c.length)return(t+1)%(c.length>>1);let f=t,s=Iu(n-c[2*t],2)+Iu(e-c[2*t+1],2);const l=r[t];let h=l;do{let r=u[h];const l=Iu(n-c[2*r],2)+Iu(e-c[2*r+1],2);if(l9999?"+"+Ku(n,6):Ku(n,4))+"-"+Ku(t.getUTCMonth()+1,2)+"-"+Ku(t.getUTCDate(),2)+(o?"T"+Ku(e,2)+":"+Ku(r,2)+":"+Ku(i,2)+"."+Ku(o,3)+"Z":i?"T"+Ku(e,2)+":"+Ku(r,2)+":"+Ku(i,2)+"Z":r||e?"T"+Ku(e,2)+":"+Ku(r,2)+"Z":"")}function Ju(t){var n=new RegExp('["'+t+"\n\r]"),e=t.charCodeAt(0);function r(t,n){var r,i=[],o=t.length,a=0,u=0,c=o<=0,f=!1;function s(){if(c)return Hu;if(f)return f=!1,ju;var n,r,i=a;if(t.charCodeAt(i)===Xu){for(;a++=o?c=!0:(r=t.charCodeAt(a++))===Gu?f=!0:r===Vu&&(f=!0,t.charCodeAt(a)===Gu&&++a),t.slice(i+1,n-1).replace(/""/g,'"')}for(;amc(n,e).then((n=>(new DOMParser).parseFromString(n,t)))}var Sc=Ac("application/xml"),Ec=Ac("text/html"),Nc=Ac("image/svg+xml");function kc(t,n,e,r){if(isNaN(n)||isNaN(e))return t;var i,o,a,u,c,f,s,l,h,d=t._root,p={data:r},g=t._x0,y=t._y0,v=t._x1,_=t._y1;if(!d)return t._root=p,t;for(;d.length;)if((f=n>=(o=(g+v)/2))?g=o:v=o,(s=e>=(a=(y+_)/2))?y=a:_=a,i=d,!(d=d[l=s<<1|f]))return i[l]=p,t;if(u=+t._x.call(null,d.data),c=+t._y.call(null,d.data),n===u&&e===c)return p.next=d,i?i[l]=p:t._root=p,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(f=n>=(o=(g+v)/2))?g=o:v=o,(s=e>=(a=(y+_)/2))?y=a:_=a}while((l=s<<1|f)==(h=(c>=a)<<1|u>=o));return i[h]=d,i[l]=p,t}function Cc(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i}function Pc(t){return t[0]}function zc(t){return t[1]}function $c(t,n,e){var r=new Dc(null==n?Pc:n,null==e?zc:e,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function Dc(t,n,e,r,i,o){this._x=t,this._y=n,this._x0=e,this._y0=r,this._x1=i,this._y1=o,this._root=void 0}function Rc(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}var Fc=$c.prototype=Dc.prototype;function qc(t){return function(){return t}}function Uc(t){return 1e-6*(t()-.5)}function Ic(t){return t.x+t.vx}function Oc(t){return t.y+t.vy}function Bc(t){return t.index}function Yc(t,n){var e=t.get(n);if(!e)throw new Error("node not found: "+n);return e}Fc.copy=function(){var t,n,e=new Dc(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=Rc(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=Rc(n));return e},Fc.add=function(t){const n=+this._x.call(null,t),e=+this._y.call(null,t);return kc(this.cover(n,e),n,e,t)},Fc.addAll=function(t){var n,e,r,i,o=t.length,a=new Array(o),u=new Array(o),c=1/0,f=1/0,s=-1/0,l=-1/0;for(e=0;es&&(s=r),il&&(l=i));if(c>s||f>l)return this;for(this.cover(c,f).cover(s,l),e=0;et||t>=i||r>n||n>=o;)switch(u=(nh||(o=c.y0)>d||(a=c.x1)=v)<<1|t>=y)&&(c=p[p.length-1],p[p.length-1]=p[p.length-1-f],p[p.length-1-f]=c)}else{var _=t-+this._x.call(null,g.data),b=n-+this._y.call(null,g.data),m=_*_+b*b;if(m=(u=(p+y)/2))?p=u:y=u,(s=a>=(c=(g+v)/2))?g=c:v=c,n=d,!(d=d[l=s<<1|f]))return this;if(!d.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(d=n[0]||n[1]||n[2]||n[3])&&d===(n[3]||n[2]||n[1]||n[0])&&!d.length&&(e?e[h]=d:this._root=d),this):(this._root=i,this)},Fc.removeAll=function(t){for(var n=0,e=t.length;n1?r[0]+r.slice(2):r,+t.slice(e+1)]}function Zc(t){return(t=Wc(Math.abs(t)))?t[1]:NaN}var Kc,Qc=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function Jc(t){if(!(n=Qc.exec(t)))throw new Error("invalid format: "+t);var n;return new tf({fill:n[1],align:n[2],sign:n[3],symbol:n[4],zero:n[5],width:n[6],comma:n[7],precision:n[8]&&n[8].slice(1),trim:n[9],type:n[10]})}function tf(t){this.fill=void 0===t.fill?" ":t.fill+"",this.align=void 0===t.align?">":t.align+"",this.sign=void 0===t.sign?"-":t.sign+"",this.symbol=void 0===t.symbol?"":t.symbol+"",this.zero=!!t.zero,this.width=void 0===t.width?void 0:+t.width,this.comma=!!t.comma,this.precision=void 0===t.precision?void 0:+t.precision,this.trim=!!t.trim,this.type=void 0===t.type?"":t.type+""}function nf(t,n){var e=Wc(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}Jc.prototype=tf.prototype,tf.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};var ef={"%":(t,n)=>(100*t).toFixed(n),b:t=>Math.round(t).toString(2),c:t=>t+"",d:function(t){return Math.abs(t=Math.round(t))>=1e21?t.toLocaleString("en").replace(/,/g,""):t.toString(10)},e:(t,n)=>t.toExponential(n),f:(t,n)=>t.toFixed(n),g:(t,n)=>t.toPrecision(n),o:t=>Math.round(t).toString(8),p:(t,n)=>nf(100*t,n),r:nf,s:function(t,n){var e=Wc(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(Kc=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,a=r.length;return o===a?r:o>a?r+new Array(o-a+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+Wc(t,Math.max(0,n+o-1))[0]},X:t=>Math.round(t).toString(16).toUpperCase(),x:t=>Math.round(t).toString(16)};function rf(t){return t}var of,af=Array.prototype.map,uf=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function cf(t){var n,e,r=void 0===t.grouping||void 0===t.thousands?rf:(n=af.call(t.grouping,Number),e=t.thousands+"",function(t,r){for(var i=t.length,o=[],a=0,u=n[0],c=0;i>0&&u>0&&(c+u+1>r&&(u=Math.max(1,r-c)),o.push(t.substring(i-=u,i+u)),!((c+=u+1)>r));)u=n[a=(a+1)%n.length];return o.reverse().join(e)}),i=void 0===t.currency?"":t.currency[0]+"",o=void 0===t.currency?"":t.currency[1]+"",a=void 0===t.decimal?".":t.decimal+"",u=void 0===t.numerals?rf:function(t){return function(n){return n.replace(/[0-9]/g,(function(n){return t[+n]}))}}(af.call(t.numerals,String)),c=void 0===t.percent?"%":t.percent+"",f=void 0===t.minus?"−":t.minus+"",s=void 0===t.nan?"NaN":t.nan+"";function l(t){var n=(t=Jc(t)).fill,e=t.align,l=t.sign,h=t.symbol,d=t.zero,p=t.width,g=t.comma,y=t.precision,v=t.trim,_=t.type;"n"===_?(g=!0,_="g"):ef[_]||(void 0===y&&(y=12),v=!0,_="g"),(d||"0"===n&&"="===e)&&(d=!0,n="0",e="=");var b="$"===h?i:"#"===h&&/[boxX]/.test(_)?"0"+_.toLowerCase():"",m="$"===h?o:/[%p]/.test(_)?c:"",x=ef[_],w=/[defgprs%]/.test(_);function M(t){var i,o,c,h=b,M=m;if("c"===_)M=x(t)+M,t="";else{var T=(t=+t)<0||1/t<0;if(t=isNaN(t)?s:x(Math.abs(t),y),v&&(t=function(t){t:for(var n,e=t.length,r=1,i=-1;r0&&(i=0)}return i>0?t.slice(0,i)+t.slice(n+1):t}(t)),T&&0==+t&&"+"!==l&&(T=!1),h=(T?"("===l?l:f:"-"===l||"("===l?"":l)+h,M=("s"===_?uf[8+Kc/3]:"")+M+(T&&"("===l?")":""),w)for(i=-1,o=t.length;++i(c=t.charCodeAt(i))||c>57){M=(46===c?a+t.slice(i+1):t.slice(i))+M,t=t.slice(0,i);break}}g&&!d&&(t=r(t,1/0));var A=h.length+t.length+M.length,S=A>1)+h+t+M+S.slice(A);break;default:t=S+h+t+M}return u(t)}return y=void 0===y?6:/[gprs]/.test(_)?Math.max(1,Math.min(21,y)):Math.max(0,Math.min(20,y)),M.toString=function(){return t+""},M}return{format:l,formatPrefix:function(t,n){var e=l(((t=Jc(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(Zc(n)/3))),i=Math.pow(10,-r),o=uf[8+r/3];return function(t){return e(i*t)+o}}}}function ff(n){return of=cf(n),t.format=of.format,t.formatPrefix=of.formatPrefix,of}function sf(t){return Math.max(0,-Zc(Math.abs(t)))}function lf(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Zc(n)/3)))-Zc(Math.abs(t)))}function hf(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Zc(n)-Zc(t))+1}t.format=void 0,t.formatPrefix=void 0,ff({thousands:",",grouping:[3],currency:["$",""]});var df=1e-6,pf=1e-12,gf=Math.PI,yf=gf/2,vf=gf/4,_f=2*gf,bf=180/gf,mf=gf/180,xf=Math.abs,wf=Math.atan,Mf=Math.atan2,Tf=Math.cos,Af=Math.ceil,Sf=Math.exp,Ef=Math.hypot,Nf=Math.log,kf=Math.pow,Cf=Math.sin,Pf=Math.sign||function(t){return t>0?1:t<0?-1:0},zf=Math.sqrt,$f=Math.tan;function Df(t){return t>1?0:t<-1?gf:Math.acos(t)}function Rf(t){return t>1?yf:t<-1?-yf:Math.asin(t)}function Ff(t){return(t=Cf(t/2))*t}function qf(){}function Uf(t,n){t&&Of.hasOwnProperty(t.type)&&Of[t.type](t,n)}var If={Feature:function(t,n){Uf(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r=0?1:-1,i=r*e,o=Tf(n=(n*=mf)/2+vf),a=Cf(n),u=Vf*a,c=Gf*o+u*Tf(i),f=u*r*Cf(i);as.add(Mf(f,c)),Xf=t,Gf=o,Vf=a}function ds(t){return[Mf(t[1],t[0]),Rf(t[2])]}function ps(t){var n=t[0],e=t[1],r=Tf(e);return[r*Tf(n),r*Cf(n),Cf(e)]}function gs(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function ys(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function vs(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function _s(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function bs(t){var n=zf(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}var ms,xs,ws,Ms,Ts,As,Ss,Es,Ns,ks,Cs,Ps,zs,$s,Ds,Rs,Fs={point:qs,lineStart:Is,lineEnd:Os,polygonStart:function(){Fs.point=Bs,Fs.lineStart=Ys,Fs.lineEnd=Ls,rs=new T,cs.polygonStart()},polygonEnd:function(){cs.polygonEnd(),Fs.point=qs,Fs.lineStart=Is,Fs.lineEnd=Os,as<0?(Wf=-(Kf=180),Zf=-(Qf=90)):rs>df?Qf=90:rs<-df&&(Zf=-90),os[0]=Wf,os[1]=Kf},sphere:function(){Wf=-(Kf=180),Zf=-(Qf=90)}};function qs(t,n){is.push(os=[Wf=t,Kf=t]),nQf&&(Qf=n)}function Us(t,n){var e=ps([t*mf,n*mf]);if(es){var r=ys(es,e),i=ys([r[1],-r[0],0],r);bs(i),i=ds(i);var o,a=t-Jf,u=a>0?1:-1,c=i[0]*bf*u,f=xf(a)>180;f^(u*JfQf&&(Qf=o):f^(u*Jf<(c=(c+360)%360-180)&&cQf&&(Qf=n)),f?tjs(Wf,Kf)&&(Kf=t):js(t,Kf)>js(Wf,Kf)&&(Wf=t):Kf>=Wf?(tKf&&(Kf=t)):t>Jf?js(Wf,t)>js(Wf,Kf)&&(Kf=t):js(t,Kf)>js(Wf,Kf)&&(Wf=t)}else is.push(os=[Wf=t,Kf=t]);nQf&&(Qf=n),es=e,Jf=t}function Is(){Fs.point=Us}function Os(){os[0]=Wf,os[1]=Kf,Fs.point=qs,es=null}function Bs(t,n){if(es){var e=t-Jf;rs.add(xf(e)>180?e+(e>0?360:-360):e)}else ts=t,ns=n;cs.point(t,n),Us(t,n)}function Ys(){cs.lineStart()}function Ls(){Bs(ts,ns),cs.lineEnd(),xf(rs)>df&&(Wf=-(Kf=180)),os[0]=Wf,os[1]=Kf,es=null}function js(t,n){return(n-=t)<0?n+360:n}function Hs(t,n){return t[0]-n[0]}function Xs(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:ngf&&(t-=Math.round(t/_f)*_f),[t,n]}function ul(t,n,e){return(t%=_f)?n||e?ol(fl(t),sl(n,e)):fl(t):n||e?sl(n,e):al}function cl(t){return function(n,e){return xf(n+=t)>gf&&(n-=Math.round(n/_f)*_f),[n,e]}}function fl(t){var n=cl(t);return n.invert=cl(-t),n}function sl(t,n){var e=Tf(t),r=Cf(t),i=Tf(n),o=Cf(n);function a(t,n){var a=Tf(n),u=Tf(t)*a,c=Cf(t)*a,f=Cf(n),s=f*e+u*r;return[Mf(c*i-s*o,u*e-f*r),Rf(s*i+c*o)]}return a.invert=function(t,n){var a=Tf(n),u=Tf(t)*a,c=Cf(t)*a,f=Cf(n),s=f*i-c*o;return[Mf(c*i+f*o,u*e+s*r),Rf(s*e-u*r)]},a}function ll(t){function n(n){return(n=t(n[0]*mf,n[1]*mf))[0]*=bf,n[1]*=bf,n}return t=ul(t[0]*mf,t[1]*mf,t.length>2?t[2]*mf:0),n.invert=function(n){return(n=t.invert(n[0]*mf,n[1]*mf))[0]*=bf,n[1]*=bf,n},n}function hl(t,n,e,r,i,o){if(e){var a=Tf(n),u=Cf(n),c=r*e;null==i?(i=n+r*_f,o=n-c/2):(i=dl(a,i),o=dl(a,o),(r>0?io)&&(i+=r*_f));for(var f,s=i;r>0?s>o:s1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}}function gl(t,n){return xf(t[0]-n[0])=0;--o)i.point((s=f[o])[0],s[1]);else r(h.x,h.p.x,-1,i);h=h.p}f=(h=h.o).z,d=!d}while(!h.v);i.lineEnd()}}}function _l(t){if(n=t.length){for(var n,e,r=0,i=t[0];++r=0?1:-1,E=S*A,N=E>gf,k=y*w;if(c.add(Mf(k*S*Cf(E),v*M+k*Tf(E))),a+=N?A+S*_f:A,N^p>=e^m>=e){var C=ys(ps(d),ps(b));bs(C);var P=ys(o,C);bs(P);var z=(N^A>=0?-1:1)*Rf(P[2]);(r>z||r===z&&(C[0]||C[1]))&&(u+=N^A>=0?1:-1)}}return(a<-df||a0){for(l||(i.polygonStart(),l=!0),i.lineStart(),t=0;t1&&2&c&&h.push(h.pop().concat(h.shift())),a.push(h.filter(wl))}return h}}function wl(t){return t.length>1}function Ml(t,n){return((t=t.x)[0]<0?t[1]-yf-df:yf-t[1])-((n=n.x)[0]<0?n[1]-yf-df:yf-n[1])}al.invert=al;var Tl=xl((function(){return!0}),(function(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,a){var u=o>0?gf:-gf,c=xf(o-e);xf(c-gf)0?yf:-yf),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),t.point(o,r),n=0):i!==u&&c>=gf&&(xf(e-i)df?wf((Cf(n)*(o=Tf(r))*Cf(e)-Cf(r)*(i=Tf(n))*Cf(t))/(i*o*a)):(n+r)/2}(e,r,o,a),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),n=0),t.point(e=o,r=a),i=u},lineEnd:function(){t.lineEnd(),e=r=NaN},clean:function(){return 2-n}}}),(function(t,n,e,r){var i;if(null==t)i=e*yf,r.point(-gf,i),r.point(0,i),r.point(gf,i),r.point(gf,0),r.point(gf,-i),r.point(0,-i),r.point(-gf,-i),r.point(-gf,0),r.point(-gf,i);else if(xf(t[0]-n[0])>df){var o=t[0]0,i=xf(n)>df;function o(t,e){return Tf(t)*Tf(e)>n}function a(t,e,r){var i=[1,0,0],o=ys(ps(t),ps(e)),a=gs(o,o),u=o[0],c=a-u*u;if(!c)return!r&&t;var f=n*a/c,s=-n*u/c,l=ys(i,o),h=_s(i,f);vs(h,_s(o,s));var d=l,p=gs(h,d),g=gs(d,d),y=p*p-g*(gs(h,h)-1);if(!(y<0)){var v=zf(y),_=_s(d,(-p-v)/g);if(vs(_,h),_=ds(_),!r)return _;var b,m=t[0],x=e[0],w=t[1],M=e[1];x0^_[1]<(xf(_[0]-m)gf^(m<=_[0]&&_[0]<=x)){var S=_s(d,(-p+v)/g);return vs(S,h),[_,ds(S)]}}}function u(n,e){var i=r?t:gf-t,o=0;return n<-i?o|=1:n>i&&(o|=2),e<-i?o|=4:e>i&&(o|=8),o}return xl(o,(function(t){var n,e,c,f,s;return{lineStart:function(){f=c=!1,s=1},point:function(l,h){var d,p=[l,h],g=o(l,h),y=r?g?0:u(l,h):g?u(l+(l<0?gf:-gf),h):0;if(!n&&(f=c=g)&&t.lineStart(),g!==c&&(!(d=a(n,p))||gl(n,d)||gl(p,d))&&(p[2]=1),g!==c)s=0,g?(t.lineStart(),d=a(p,n),t.point(d[0],d[1])):(d=a(n,p),t.point(d[0],d[1],2),t.lineEnd()),n=d;else if(i&&n&&r^g){var v;y&e||!(v=a(p,n,!0))||(s=0,r?(t.lineStart(),t.point(v[0][0],v[0][1]),t.point(v[1][0],v[1][1]),t.lineEnd()):(t.point(v[1][0],v[1][1]),t.lineEnd(),t.lineStart(),t.point(v[0][0],v[0][1],3)))}!g||n&&gl(n,p)||t.point(p[0],p[1]),n=p,c=g,e=y},lineEnd:function(){c&&t.lineEnd(),n=null},clean:function(){return s|(f&&c)<<1}}}),(function(n,r,i,o){hl(o,t,e,i,n,r)}),r?[0,-t]:[-gf,t-gf])}var Sl,El,Nl,kl,Cl=1e9,Pl=-Cl;function zl(t,n,e,r){function i(i,o){return t<=i&&i<=e&&n<=o&&o<=r}function o(i,o,u,f){var s=0,l=0;if(null==i||(s=a(i,u))!==(l=a(o,u))||c(i,o)<0^u>0)do{f.point(0===s||3===s?t:e,s>1?r:n)}while((s=(s+u+4)%4)!==l);else f.point(o[0],o[1])}function a(r,i){return xf(r[0]-t)0?0:3:xf(r[0]-e)0?2:1:xf(r[1]-n)0?1:0:i>0?3:2}function u(t,n){return c(t.x,n.x)}function c(t,n){var e=a(t,1),r=a(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(a){var c,f,s,l,h,d,p,g,y,v,_,b=a,m=pl(),x={point:w,lineStart:function(){x.point=M,f&&f.push(s=[]);v=!0,y=!1,p=g=NaN},lineEnd:function(){c&&(M(l,h),d&&y&&m.rejoin(),c.push(m.result()));x.point=w,y&&b.lineEnd()},polygonStart:function(){b=m,c=[],f=[],_=!0},polygonEnd:function(){var n=function(){for(var n=0,e=0,i=f.length;er&&(h-o)*(r-a)>(d-a)*(t-o)&&++n:d<=r&&(h-o)*(r-a)<(d-a)*(t-o)&&--n;return n}(),e=_&&n,i=(c=ft(c)).length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),o(null,null,1,a),a.lineEnd()),i&&vl(c,u,n,o,a),a.polygonEnd());b=a,c=f=s=null}};function w(t,n){i(t,n)&&b.point(t,n)}function M(o,a){var u=i(o,a);if(f&&s.push([o,a]),v)l=o,h=a,d=u,v=!1,u&&(b.lineStart(),b.point(o,a));else if(u&&y)b.point(o,a);else{var c=[p=Math.max(Pl,Math.min(Cl,p)),g=Math.max(Pl,Math.min(Cl,g))],m=[o=Math.max(Pl,Math.min(Cl,o)),a=Math.max(Pl,Math.min(Cl,a))];!function(t,n,e,r,i,o){var a,u=t[0],c=t[1],f=0,s=1,l=n[0]-u,h=n[1]-c;if(a=e-u,l||!(a>0)){if(a/=l,l<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=i-u,l||!(a<0)){if(a/=l,l<0){if(a>s)return;a>f&&(f=a)}else if(l>0){if(a0)){if(a/=h,h<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=o-c,h||!(a<0)){if(a/=h,h<0){if(a>s)return;a>f&&(f=a)}else if(h>0){if(a0&&(t[0]=u+f*l,t[1]=c+f*h),s<1&&(n[0]=u+s*l,n[1]=c+s*h),!0}}}}}(c,m,t,n,e,r)?u&&(b.lineStart(),b.point(o,a),_=!1):(y||(b.lineStart(),b.point(c[0],c[1])),b.point(m[0],m[1]),u||b.lineEnd(),_=!1)}p=o,g=a,y=u}return x}}var $l={sphere:qf,point:qf,lineStart:function(){$l.point=Rl,$l.lineEnd=Dl},lineEnd:qf,polygonStart:qf,polygonEnd:qf};function Dl(){$l.point=$l.lineEnd=qf}function Rl(t,n){El=t*=mf,Nl=Cf(n*=mf),kl=Tf(n),$l.point=Fl}function Fl(t,n){t*=mf;var e=Cf(n*=mf),r=Tf(n),i=xf(t-El),o=Tf(i),a=r*Cf(i),u=kl*e-Nl*r*o,c=Nl*e+kl*r*o;Sl.add(Mf(zf(a*a+u*u),c)),El=t,Nl=e,kl=r}function ql(t){return Sl=new T,Lf(t,$l),+Sl}var Ul=[null,null],Il={type:"LineString",coordinates:Ul};function Ol(t,n){return Ul[0]=t,Ul[1]=n,ql(Il)}var Bl={Feature:function(t,n){return Ll(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r0&&(i=Ol(t[o],t[o-1]))>0&&e<=i&&r<=i&&(e+r-i)*(1-Math.pow((e-r)/i,2))df})).map(c)).concat(lt(Af(o/d)*d,i,d).filter((function(t){return xf(t%g)>df})).map(f))}return v.lines=function(){return _().map((function(t){return{type:"LineString",coordinates:t}}))},v.outline=function(){return{type:"Polygon",coordinates:[s(r).concat(l(a).slice(1),s(e).reverse().slice(1),l(u).reverse().slice(1))]}},v.extent=function(t){return arguments.length?v.extentMajor(t).extentMinor(t):v.extentMinor()},v.extentMajor=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],u=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),u>a&&(t=u,u=a,a=t),v.precision(y)):[[r,u],[e,a]]},v.extentMinor=function(e){return arguments.length?(n=+e[0][0],t=+e[1][0],o=+e[0][1],i=+e[1][1],n>t&&(e=n,n=t,t=e),o>i&&(e=o,o=i,i=e),v.precision(y)):[[n,o],[t,i]]},v.step=function(t){return arguments.length?v.stepMajor(t).stepMinor(t):v.stepMinor()},v.stepMajor=function(t){return arguments.length?(p=+t[0],g=+t[1],v):[p,g]},v.stepMinor=function(t){return arguments.length?(h=+t[0],d=+t[1],v):[h,d]},v.precision=function(h){return arguments.length?(y=+h,c=Wl(o,i,90),f=Zl(n,t,y),s=Wl(u,a,90),l=Zl(r,e,y),v):y},v.extentMajor([[-180,-90+df],[180,90-df]]).extentMinor([[-180,-80-df],[180,80+df]])}var Ql,Jl,th,nh,eh=t=>t,rh=new T,ih=new T,oh={point:qf,lineStart:qf,lineEnd:qf,polygonStart:function(){oh.lineStart=ah,oh.lineEnd=fh},polygonEnd:function(){oh.lineStart=oh.lineEnd=oh.point=qf,rh.add(xf(ih)),ih=new T},result:function(){var t=rh/2;return rh=new T,t}};function ah(){oh.point=uh}function uh(t,n){oh.point=ch,Ql=th=t,Jl=nh=n}function ch(t,n){ih.add(nh*t-th*n),th=t,nh=n}function fh(){ch(Ql,Jl)}var sh=oh,lh=1/0,hh=lh,dh=-lh,ph=dh,gh={point:function(t,n){tdh&&(dh=t);nph&&(ph=n)},lineStart:qf,lineEnd:qf,polygonStart:qf,polygonEnd:qf,result:function(){var t=[[lh,hh],[dh,ph]];return dh=ph=-(hh=lh=1/0),t}};var yh,vh,_h,bh,mh=gh,xh=0,wh=0,Mh=0,Th=0,Ah=0,Sh=0,Eh=0,Nh=0,kh=0,Ch={point:Ph,lineStart:zh,lineEnd:Rh,polygonStart:function(){Ch.lineStart=Fh,Ch.lineEnd=qh},polygonEnd:function(){Ch.point=Ph,Ch.lineStart=zh,Ch.lineEnd=Rh},result:function(){var t=kh?[Eh/kh,Nh/kh]:Sh?[Th/Sh,Ah/Sh]:Mh?[xh/Mh,wh/Mh]:[NaN,NaN];return xh=wh=Mh=Th=Ah=Sh=Eh=Nh=kh=0,t}};function Ph(t,n){xh+=t,wh+=n,++Mh}function zh(){Ch.point=$h}function $h(t,n){Ch.point=Dh,Ph(_h=t,bh=n)}function Dh(t,n){var e=t-_h,r=n-bh,i=zf(e*e+r*r);Th+=i*(_h+t)/2,Ah+=i*(bh+n)/2,Sh+=i,Ph(_h=t,bh=n)}function Rh(){Ch.point=Ph}function Fh(){Ch.point=Uh}function qh(){Ih(yh,vh)}function Uh(t,n){Ch.point=Ih,Ph(yh=_h=t,vh=bh=n)}function Ih(t,n){var e=t-_h,r=n-bh,i=zf(e*e+r*r);Th+=i*(_h+t)/2,Ah+=i*(bh+n)/2,Sh+=i,Eh+=(i=bh*t-_h*n)*(_h+t),Nh+=i*(bh+n),kh+=3*i,Ph(_h=t,bh=n)}var Oh=Ch;function Bh(t){this._context=t}Bh.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,_f)}},result:qf};var Yh,Lh,jh,Hh,Xh,Gh=new T,Vh={point:qf,lineStart:function(){Vh.point=Wh},lineEnd:function(){Yh&&Zh(Lh,jh),Vh.point=qf},polygonStart:function(){Yh=!0},polygonEnd:function(){Yh=null},result:function(){var t=+Gh;return Gh=new T,t}};function Wh(t,n){Vh.point=Zh,Lh=Hh=t,jh=Xh=n}function Zh(t,n){Hh-=t,Xh-=n,Gh.add(zf(Hh*Hh+Xh*Xh)),Hh=t,Xh=n}var Kh=Vh;let Qh,Jh,td,nd;class ed{constructor(t){this._append=null==t?rd:function(t){const n=Math.floor(t);if(!(n>=0))throw new RangeError(`invalid digits: ${t}`);if(n>15)return rd;if(n!==Qh){const t=10**n;Qh=n,Jh=function(n){let e=1;this._+=n[0];for(const r=n.length;e4*n&&g--){var m=a+h,x=u+d,w=c+p,M=zf(m*m+x*x+w*w),T=Rf(w/=M),A=xf(xf(w)-1)n||xf((v*k+_*C)/b-.5)>.3||a*h+u*d+c*p2?t[2]%360*mf:0,k()):[y*bf,v*bf,_*bf]},E.angle=function(t){return arguments.length?(b=t%360*mf,k()):b*bf},E.reflectX=function(t){return arguments.length?(m=t?-1:1,k()):m<0},E.reflectY=function(t){return arguments.length?(x=t?-1:1,k()):x<0},E.precision=function(t){return arguments.length?(a=dd(u,S=t*t),C()):zf(S)},E.fitExtent=function(t,n){return ud(E,t,n)},E.fitSize=function(t,n){return cd(E,t,n)},E.fitWidth=function(t,n){return fd(E,t,n)},E.fitHeight=function(t,n){return sd(E,t,n)},function(){return n=t.apply(this,arguments),E.invert=n.invert&&N,k()}}function _d(t){var n=0,e=gf/3,r=vd(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*mf,e=t[1]*mf):[n*bf,e*bf]},i}function bd(t,n){var e=Cf(t),r=(e+Cf(n))/2;if(xf(r)0?n<-yf+df&&(n=-yf+df):n>yf-df&&(n=yf-df);var e=i/kf(Nd(n),r);return[e*Cf(r*t),i-e*Tf(r*t)]}return o.invert=function(t,n){var e=i-n,o=Pf(r)*zf(t*t+e*e),a=Mf(t,xf(e))*Pf(e);return e*r<0&&(a-=gf*Pf(t)*Pf(e)),[a/r,2*wf(kf(i/o,1/r))-yf]},o}function Cd(t,n){return[t,n]}function Pd(t,n){var e=Tf(t),r=t===n?Cf(t):(e-Tf(n))/(n-t),i=e/r+t;if(xf(r)=0;)n+=e[r].value;else n=1;t.value=n}function Gd(t,n){t instanceof Map?(t=[void 0,t],void 0===n&&(n=Wd)):void 0===n&&(n=Vd);for(var e,r,i,o,a,u=new Qd(t),c=[u];e=c.pop();)if((i=n(e.data))&&(a=(i=Array.from(i)).length))for(e.children=i,o=a-1;o>=0;--o)c.push(r=i[o]=new Qd(i[o])),r.parent=e,r.depth=e.depth+1;return u.eachBefore(Kd)}function Vd(t){return t.children}function Wd(t){return Array.isArray(t)?t[1]:null}function Zd(t){void 0!==t.data.value&&(t.value=t.data.value),t.data=t.data.data}function Kd(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function Qd(t){this.data=t,this.depth=this.height=0,this.parent=null}function Jd(t){return null==t?null:tp(t)}function tp(t){if("function"!=typeof t)throw new Error;return t}function np(){return 0}function ep(t){return function(){return t}}qd.invert=function(t,n){for(var e,r=n,i=r*r,o=i*i*i,a=0;a<12&&(o=(i=(r-=e=(r*(zd+$d*i+o*(Dd+Rd*i))-n)/(zd+3*$d*i+o*(7*Dd+9*Rd*i)))*r)*i*i,!(xf(e)df&&--i>0);return[t/(.8707+(o=r*r)*(o*(o*o*o*(.003971-.001529*o)-.013791)-.131979)),r]},Od.invert=Md(Rf),Bd.invert=Md((function(t){return 2*wf(t)})),Yd.invert=function(t,n){return[-n,2*wf(Sf(t))-yf]},Qd.prototype=Gd.prototype={constructor:Qd,count:function(){return this.eachAfter(Xd)},each:function(t,n){let e=-1;for(const r of this)t.call(n,r,++e,this);return this},eachAfter:function(t,n){for(var e,r,i,o=this,a=[o],u=[],c=-1;o=a.pop();)if(u.push(o),e=o.children)for(r=0,i=e.length;r=0;--r)o.push(e[r]);return this},find:function(t,n){let e=-1;for(const r of this)if(t.call(n,r,++e,this))return r},sum:function(t){return this.eachAfter((function(n){for(var e=+t(n.data)||0,r=n.children,i=r&&r.length;--i>=0;)e+=r[i].value;n.value=e}))},sort:function(t){return this.eachBefore((function(n){n.children&&n.children.sort(t)}))},path:function(t){for(var n=this,e=function(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;t=e.pop(),n=r.pop();for(;t===n;)i=t,t=e.pop(),n=r.pop();return i}(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},descendants:function(){return Array.from(this)},leaves:function(){var t=[];return this.eachBefore((function(n){n.children||t.push(n)})),t},links:function(){var t=this,n=[];return t.each((function(e){e!==t&&n.push({source:e.parent,target:e})})),n},copy:function(){return Gd(this).eachBefore(Zd)},[Symbol.iterator]:function*(){var t,n,e,r,i=this,o=[i];do{for(t=o.reverse(),o=[];i=t.pop();)if(yield i,n=i.children)for(e=0,r=n.length;e(t=(rp*t+ip)%op)/op}function up(t,n){for(var e,r,i=0,o=(t=function(t,n){let e,r,i=t.length;for(;i;)r=n()*i--|0,e=t[i],t[i]=t[r],t[r]=e;return t}(Array.from(t),n)).length,a=[];i0&&e*e>r*r+i*i}function lp(t,n){for(var e=0;e1e-6?(E+Math.sqrt(E*E-4*S*N))/(2*S):N/E);return{x:r+w+M*k,y:i+T+A*k,r:k}}function gp(t,n,e){var r,i,o,a,u=t.x-n.x,c=t.y-n.y,f=u*u+c*c;f?(i=n.r+e.r,i*=i,a=t.r+e.r,i>(a*=a)?(r=(f+a-i)/(2*f),o=Math.sqrt(Math.max(0,a/f-r*r)),e.x=t.x-r*u-o*c,e.y=t.y-r*c+o*u):(r=(f+i-a)/(2*f),o=Math.sqrt(Math.max(0,i/f-r*r)),e.x=n.x+r*u-o*c,e.y=n.y+r*c+o*u)):(e.x=n.x+e.r,e.y=n.y)}function yp(t,n){var e=t.r+n.r-1e-6,r=n.x-t.x,i=n.y-t.y;return e>0&&e*e>r*r+i*i}function vp(t){var n=t._,e=t.next._,r=n.r+e.r,i=(n.x*e.r+e.x*n.r)/r,o=(n.y*e.r+e.y*n.r)/r;return i*i+o*o}function _p(t){this._=t,this.next=null,this.previous=null}function bp(t,n){if(!(o=(t=function(t){return"object"==typeof t&&"length"in t?t:Array.from(t)}(t)).length))return 0;var e,r,i,o,a,u,c,f,s,l,h;if((e=t[0]).x=0,e.y=0,!(o>1))return e.r;if(r=t[1],e.x=-r.r,r.x=e.r,r.y=0,!(o>2))return e.r+r.r;gp(r,e,i=t[2]),e=new _p(e),r=new _p(r),i=new _p(i),e.next=i.previous=r,r.next=e.previous=i,i.next=r.previous=e;t:for(c=3;c1&&!zp(t,n););return t.slice(0,n)}function zp(t,n){if("/"===t[n]){let e=0;for(;n>0&&"\\"===t[--n];)++e;if(!(1&e))return!0}return!1}function $p(t,n){return t.parent===n.parent?1:2}function Dp(t){var n=t.children;return n?n[0]:t.t}function Rp(t){var n=t.children;return n?n[n.length-1]:t.t}function Fp(t,n,e){var r=e/(n.i-t.i);n.c-=r,n.s+=e,t.c+=r,n.z+=e,n.m+=e}function qp(t,n,e){return t.a.parent===n.parent?t.a:e}function Up(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function Ip(t,n,e,r,i){for(var o,a=t.children,u=-1,c=a.length,f=t.value&&(i-e)/t.value;++uh&&(h=u),y=s*s*g,(d=Math.max(h/y,y/l))>p){s-=u;break}p=d}v.push(a={value:s,dice:c1?n:1)},e}(Op);var Lp=function t(n){function e(t,e,r,i,o){if((a=t._squarify)&&a.ratio===n)for(var a,u,c,f,s,l=-1,h=a.length,d=t.value;++l1?n:1)},e}(Op);function jp(t,n,e){return(n[0]-t[0])*(e[1]-t[1])-(n[1]-t[1])*(e[0]-t[0])}function Hp(t,n){return t[0]-n[0]||t[1]-n[1]}function Xp(t){const n=t.length,e=[0,1];let r,i=2;for(r=2;r1&&jp(t[e[i-2]],t[e[i-1]],t[r])<=0;)--i;e[i++]=r}return e.slice(0,i)}var Gp=Math.random,Vp=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,1===arguments.length?(e=t,t=0):e-=t,function(){return n()*e+t}}return e.source=t,e}(Gp),Wp=function t(n){function e(t,e){return arguments.length<2&&(e=t,t=0),t=Math.floor(t),e=Math.floor(e)-t,function(){return Math.floor(n()*e+t)}}return e.source=t,e}(Gp),Zp=function t(n){function e(t,e){var r,i;return t=null==t?0:+t,e=null==e?1:+e,function(){var o;if(null!=r)o=r,r=null;else do{r=2*n()-1,o=2*n()-1,i=r*r+o*o}while(!i||i>1);return t+e*o*Math.sqrt(-2*Math.log(i)/i)}}return e.source=t,e}(Gp),Kp=function t(n){var e=Zp.source(n);function r(){var t=e.apply(this,arguments);return function(){return Math.exp(t())}}return r.source=t,r}(Gp),Qp=function t(n){function e(t){return(t=+t)<=0?()=>0:function(){for(var e=0,r=t;r>1;--r)e+=n();return e+r*n()}}return e.source=t,e}(Gp),Jp=function t(n){var e=Qp.source(n);function r(t){if(0==(t=+t))return n;var r=e(t);return function(){return r()/t}}return r.source=t,r}(Gp),tg=function t(n){function e(t){return function(){return-Math.log1p(-n())/t}}return e.source=t,e}(Gp),ng=function t(n){function e(t){if((t=+t)<0)throw new RangeError("invalid alpha");return t=1/-t,function(){return Math.pow(1-n(),t)}}return e.source=t,e}(Gp),eg=function t(n){function e(t){if((t=+t)<0||t>1)throw new RangeError("invalid p");return function(){return Math.floor(n()+t)}}return e.source=t,e}(Gp),rg=function t(n){function e(t){if((t=+t)<0||t>1)throw new RangeError("invalid p");return 0===t?()=>1/0:1===t?()=>1:(t=Math.log1p(-t),function(){return 1+Math.floor(Math.log1p(-n())/t)})}return e.source=t,e}(Gp),ig=function t(n){var e=Zp.source(n)();function r(t,r){if((t=+t)<0)throw new RangeError("invalid k");if(0===t)return()=>0;if(r=null==r?1:+r,1===t)return()=>-Math.log1p(-n())*r;var i=(t<1?t+1:t)-1/3,o=1/(3*Math.sqrt(i)),a=t<1?()=>Math.pow(n(),1/t):()=>1;return function(){do{do{var t=e(),u=1+o*t}while(u<=0);u*=u*u;var c=1-n()}while(c>=1-.0331*t*t*t*t&&Math.log(c)>=.5*t*t+i*(1-u+Math.log(u)));return i*u*a()*r}}return r.source=t,r}(Gp),og=function t(n){var e=ig.source(n);function r(t,n){var r=e(t),i=e(n);return function(){var t=r();return 0===t?0:t/(t+i())}}return r.source=t,r}(Gp),ag=function t(n){var e=rg.source(n),r=og.source(n);function i(t,n){return t=+t,(n=+n)>=1?()=>t:n<=0?()=>0:function(){for(var i=0,o=t,a=n;o*a>16&&o*(1-a)>16;){var u=Math.floor((o+1)*a),c=r(u,o-u+1)();c<=a?(i+=u,o-=u,a=(a-c)/(1-c)):(o=u-1,a/=c)}for(var f=a<.5,s=e(f?a:1-a),l=s(),h=0;l<=o;++h)l+=s();return i+(f?h:o-h)}}return i.source=t,i}(Gp),ug=function t(n){function e(t,e,r){var i;return 0==(t=+t)?i=t=>-Math.log(t):(t=1/t,i=n=>Math.pow(n,t)),e=null==e?0:+e,r=null==r?1:+r,function(){return e+r*i(-Math.log1p(-n()))}}return e.source=t,e}(Gp),cg=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,function(){return t+e*Math.tan(Math.PI*n())}}return e.source=t,e}(Gp),fg=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,function(){var r=n();return t+e*Math.log(r/(1-r))}}return e.source=t,e}(Gp),sg=function t(n){var e=ig.source(n),r=ag.source(n);function i(t){return function(){for(var i=0,o=t;o>16;){var a=Math.floor(.875*o),u=e(a)();if(u>o)return i+r(a-1,o/u)();i+=a,o-=u}for(var c=-Math.log1p(-n()),f=0;c<=o;++f)c-=Math.log1p(-n());return i+f}}return i.source=t,i}(Gp);const lg=1/4294967296;function hg(t,n){switch(arguments.length){case 0:break;case 1:this.range(t);break;default:this.range(n).domain(t)}return this}function dg(t,n){switch(arguments.length){case 0:break;case 1:"function"==typeof t?this.interpolator(t):this.range(t);break;default:this.domain(t),"function"==typeof n?this.interpolator(n):this.range(n)}return this}const pg=Symbol("implicit");function gg(){var t=new InternMap,n=[],e=[],r=pg;function i(i){let o=t.get(i);if(void 0===o){if(r!==pg)return r;t.set(i,o=n.push(i)-1)}return e[o%e.length]}return i.domain=function(e){if(!arguments.length)return n.slice();n=[],t=new InternMap;for(const r of e)t.has(r)||t.set(r,n.push(r)-1);return i},i.range=function(t){return arguments.length?(e=Array.from(t),i):e.slice()},i.unknown=function(t){return arguments.length?(r=t,i):r},i.copy=function(){return gg(n,e).unknown(r)},hg.apply(i,arguments),i}function yg(){var t,n,e=gg().unknown(void 0),r=e.domain,i=e.range,o=0,a=1,u=!1,c=0,f=0,s=.5;function l(){var e=r().length,l=an&&(e=t,t=n,n=e),function(e){return Math.max(t,Math.min(n,e))}}(a[0],a[t-1])),r=t>2?Mg:wg,i=o=null,l}function l(n){return null==n||isNaN(n=+n)?e:(i||(i=r(a.map(t),u,c)))(t(f(n)))}return l.invert=function(e){return f(n((o||(o=r(u,a.map(t),Yr)))(e)))},l.domain=function(t){return arguments.length?(a=Array.from(t,_g),s()):a.slice()},l.range=function(t){return arguments.length?(u=Array.from(t),s()):u.slice()},l.rangeRound=function(t){return u=Array.from(t),c=Vr,s()},l.clamp=function(t){return arguments.length?(f=!!t||mg,s()):f!==mg},l.interpolate=function(t){return arguments.length?(c=t,s()):c},l.unknown=function(t){return arguments.length?(e=t,l):e},function(e,r){return t=e,n=r,s()}}function Sg(){return Ag()(mg,mg)}function Eg(n,e,r,i){var o,a=W(n,e,r);switch((i=Jc(null==i?",f":i)).type){case"s":var u=Math.max(Math.abs(n),Math.abs(e));return null!=i.precision||isNaN(o=lf(a,u))||(i.precision=o),t.formatPrefix(i,u);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(o=hf(a,Math.max(Math.abs(n),Math.abs(e))))||(i.precision=o-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(o=sf(a))||(i.precision=o-2*("%"===i.type))}return t.format(i)}function Ng(t){var n=t.domain;return t.ticks=function(t){var e=n();return G(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){var r=n();return Eg(r[0],r[r.length-1],null==t?10:t,e)},t.nice=function(e){null==e&&(e=10);var r,i,o=n(),a=0,u=o.length-1,c=o[a],f=o[u],s=10;for(f0;){if((i=V(c,f,e))===r)return o[a]=c,o[u]=f,n(o);if(i>0)c=Math.floor(c/i)*i,f=Math.ceil(f/i)*i;else{if(!(i<0))break;c=Math.ceil(c*i)/i,f=Math.floor(f*i)/i}r=i}return t},t}function kg(t,n){var e,r=0,i=(t=t.slice()).length-1,o=t[r],a=t[i];return a-t(-n,e)}function Fg(n){const e=n(Cg,Pg),r=e.domain;let i,o,a=10;function u(){return i=function(t){return t===Math.E?Math.log:10===t&&Math.log10||2===t&&Math.log2||(t=Math.log(t),n=>Math.log(n)/t)}(a),o=function(t){return 10===t?Dg:t===Math.E?Math.exp:n=>Math.pow(t,n)}(a),r()[0]<0?(i=Rg(i),o=Rg(o),n(zg,$g)):n(Cg,Pg),e}return e.base=function(t){return arguments.length?(a=+t,u()):a},e.domain=function(t){return arguments.length?(r(t),u()):r()},e.ticks=t=>{const n=r();let e=n[0],u=n[n.length-1];const c=u0){for(;l<=h;++l)for(f=1;fu)break;p.push(s)}}else for(;l<=h;++l)for(f=a-1;f>=1;--f)if(s=l>0?f/o(-l):f*o(l),!(su)break;p.push(s)}2*p.length{if(null==n&&(n=10),null==r&&(r=10===a?"s":","),"function"!=typeof r&&(a%1||null!=(r=Jc(r)).precision||(r.trim=!0),r=t.format(r)),n===1/0)return r;const u=Math.max(1,a*n/e.ticks().length);return t=>{let n=t/o(Math.round(i(t)));return n*ar(kg(r(),{floor:t=>o(Math.floor(i(t))),ceil:t=>o(Math.ceil(i(t)))})),e}function qg(t){return function(n){return Math.sign(n)*Math.log1p(Math.abs(n/t))}}function Ug(t){return function(n){return Math.sign(n)*Math.expm1(Math.abs(n))*t}}function Ig(t){var n=1,e=t(qg(n),Ug(n));return e.constant=function(e){return arguments.length?t(qg(n=+e),Ug(n)):n},Ng(e)}function Og(t){return function(n){return n<0?-Math.pow(-n,t):Math.pow(n,t)}}function Bg(t){return t<0?-Math.sqrt(-t):Math.sqrt(t)}function Yg(t){return t<0?-t*t:t*t}function Lg(t){var n=t(mg,mg),e=1;return n.exponent=function(n){return arguments.length?1===(e=+n)?t(mg,mg):.5===e?t(Bg,Yg):t(Og(e),Og(1/e)):e},Ng(n)}function jg(){var t=Lg(Ag());return t.copy=function(){return Tg(t,jg()).exponent(t.exponent())},hg.apply(t,arguments),t}function Hg(t){return Math.sign(t)*t*t}const Xg=new Date,Gg=new Date;function Vg(t,n,e,r){function i(n){return t(n=0===arguments.length?new Date:new Date(+n)),n}return i.floor=n=>(t(n=new Date(+n)),n),i.ceil=e=>(t(e=new Date(e-1)),n(e,1),t(e),e),i.round=t=>{const n=i(t),e=i.ceil(t);return t-n(n(t=new Date(+t),null==e?1:Math.floor(e)),t),i.range=(e,r,o)=>{const a=[];if(e=i.ceil(e),o=null==o?1:Math.floor(o),!(e0))return a;let u;do{a.push(u=new Date(+e)),n(e,o),t(e)}while(uVg((n=>{if(n>=n)for(;t(n),!e(n);)n.setTime(n-1)}),((t,r)=>{if(t>=t)if(r<0)for(;++r<=0;)for(;n(t,-1),!e(t););else for(;--r>=0;)for(;n(t,1),!e(t););})),e&&(i.count=(n,r)=>(Xg.setTime(+n),Gg.setTime(+r),t(Xg),t(Gg),Math.floor(e(Xg,Gg))),i.every=t=>(t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?n=>r(n)%t==0:n=>i.count(0,n)%t==0):i:null)),i}const Wg=Vg((()=>{}),((t,n)=>{t.setTime(+t+n)}),((t,n)=>n-t));Wg.every=t=>(t=Math.floor(t),isFinite(t)&&t>0?t>1?Vg((n=>{n.setTime(Math.floor(n/t)*t)}),((n,e)=>{n.setTime(+n+e*t)}),((n,e)=>(e-n)/t)):Wg:null);const Zg=Wg.range,Kg=1e3,Qg=6e4,Jg=36e5,ty=864e5,ny=6048e5,ey=2592e6,ry=31536e6,iy=Vg((t=>{t.setTime(t-t.getMilliseconds())}),((t,n)=>{t.setTime(+t+n*Kg)}),((t,n)=>(n-t)/Kg),(t=>t.getUTCSeconds())),oy=iy.range,ay=Vg((t=>{t.setTime(t-t.getMilliseconds()-t.getSeconds()*Kg)}),((t,n)=>{t.setTime(+t+n*Qg)}),((t,n)=>(n-t)/Qg),(t=>t.getMinutes())),uy=ay.range,cy=Vg((t=>{t.setUTCSeconds(0,0)}),((t,n)=>{t.setTime(+t+n*Qg)}),((t,n)=>(n-t)/Qg),(t=>t.getUTCMinutes())),fy=cy.range,sy=Vg((t=>{t.setTime(t-t.getMilliseconds()-t.getSeconds()*Kg-t.getMinutes()*Qg)}),((t,n)=>{t.setTime(+t+n*Jg)}),((t,n)=>(n-t)/Jg),(t=>t.getHours())),ly=sy.range,hy=Vg((t=>{t.setUTCMinutes(0,0,0)}),((t,n)=>{t.setTime(+t+n*Jg)}),((t,n)=>(n-t)/Jg),(t=>t.getUTCHours())),dy=hy.range,py=Vg((t=>t.setHours(0,0,0,0)),((t,n)=>t.setDate(t.getDate()+n)),((t,n)=>(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Qg)/ty),(t=>t.getDate()-1)),gy=py.range,yy=Vg((t=>{t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCDate(t.getUTCDate()+n)}),((t,n)=>(n-t)/ty),(t=>t.getUTCDate()-1)),vy=yy.range,_y=Vg((t=>{t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCDate(t.getUTCDate()+n)}),((t,n)=>(n-t)/ty),(t=>Math.floor(t/ty))),by=_y.range;function my(t){return Vg((n=>{n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)}),((t,n)=>{t.setDate(t.getDate()+7*n)}),((t,n)=>(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Qg)/ny))}const xy=my(0),wy=my(1),My=my(2),Ty=my(3),Ay=my(4),Sy=my(5),Ey=my(6),Ny=xy.range,ky=wy.range,Cy=My.range,Py=Ty.range,zy=Ay.range,$y=Sy.range,Dy=Ey.range;function Ry(t){return Vg((n=>{n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCDate(t.getUTCDate()+7*n)}),((t,n)=>(n-t)/ny))}const Fy=Ry(0),qy=Ry(1),Uy=Ry(2),Iy=Ry(3),Oy=Ry(4),By=Ry(5),Yy=Ry(6),Ly=Fy.range,jy=qy.range,Hy=Uy.range,Xy=Iy.range,Gy=Oy.range,Vy=By.range,Wy=Yy.range,Zy=Vg((t=>{t.setDate(1),t.setHours(0,0,0,0)}),((t,n)=>{t.setMonth(t.getMonth()+n)}),((t,n)=>n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())),(t=>t.getMonth())),Ky=Zy.range,Qy=Vg((t=>{t.setUTCDate(1),t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCMonth(t.getUTCMonth()+n)}),((t,n)=>n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())),(t=>t.getUTCMonth())),Jy=Qy.range,tv=Vg((t=>{t.setMonth(0,1),t.setHours(0,0,0,0)}),((t,n)=>{t.setFullYear(t.getFullYear()+n)}),((t,n)=>n.getFullYear()-t.getFullYear()),(t=>t.getFullYear()));tv.every=t=>isFinite(t=Math.floor(t))&&t>0?Vg((n=>{n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)}),((n,e)=>{n.setFullYear(n.getFullYear()+e*t)})):null;const nv=tv.range,ev=Vg((t=>{t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)}),((t,n)=>{t.setUTCFullYear(t.getUTCFullYear()+n)}),((t,n)=>n.getUTCFullYear()-t.getUTCFullYear()),(t=>t.getUTCFullYear()));ev.every=t=>isFinite(t=Math.floor(t))&&t>0?Vg((n=>{n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)}),((n,e)=>{n.setUTCFullYear(n.getUTCFullYear()+e*t)})):null;const rv=ev.range;function iv(t,n,e,i,o,a){const u=[[iy,1,Kg],[iy,5,5e3],[iy,15,15e3],[iy,30,3e4],[a,1,Qg],[a,5,3e5],[a,15,9e5],[a,30,18e5],[o,1,Jg],[o,3,108e5],[o,6,216e5],[o,12,432e5],[i,1,ty],[i,2,1728e5],[e,1,ny],[n,1,ey],[n,3,7776e6],[t,1,ry]];function c(n,e,i){const o=Math.abs(e-n)/i,a=r((([,,t])=>t)).right(u,o);if(a===u.length)return t.every(W(n/ry,e/ry,i));if(0===a)return Wg.every(Math.max(W(n,e,i),1));const[c,f]=u[o/u[a-1][2]=12)]},q:function(t){return 1+~~(t.getMonth()/3)},Q:k_,s:C_,S:Zv,u:Kv,U:Qv,V:t_,w:n_,W:e_,x:null,X:null,y:r_,Y:o_,Z:u_,"%":N_},m={a:function(t){return a[t.getUTCDay()]},A:function(t){return o[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return u[t.getUTCMonth()]},c:null,d:c_,e:c_,f:d_,g:T_,G:S_,H:f_,I:s_,j:l_,L:h_,m:p_,M:g_,p:function(t){return i[+(t.getUTCHours()>=12)]},q:function(t){return 1+~~(t.getUTCMonth()/3)},Q:k_,s:C_,S:y_,u:v_,U:__,V:m_,w:x_,W:w_,x:null,X:null,y:M_,Y:A_,Z:E_,"%":N_},x={a:function(t,n,e){var r=d.exec(n.slice(e));return r?(t.w=p.get(r[0].toLowerCase()),e+r[0].length):-1},A:function(t,n,e){var r=l.exec(n.slice(e));return r?(t.w=h.get(r[0].toLowerCase()),e+r[0].length):-1},b:function(t,n,e){var r=v.exec(n.slice(e));return r?(t.m=_.get(r[0].toLowerCase()),e+r[0].length):-1},B:function(t,n,e){var r=g.exec(n.slice(e));return r?(t.m=y.get(r[0].toLowerCase()),e+r[0].length):-1},c:function(t,e,r){return T(t,n,e,r)},d:zv,e:zv,f:Uv,g:Nv,G:Ev,H:Dv,I:Dv,j:$v,L:qv,m:Pv,M:Rv,p:function(t,n,e){var r=f.exec(n.slice(e));return r?(t.p=s.get(r[0].toLowerCase()),e+r[0].length):-1},q:Cv,Q:Ov,s:Bv,S:Fv,u:Mv,U:Tv,V:Av,w:wv,W:Sv,x:function(t,n,r){return T(t,e,n,r)},X:function(t,n,e){return T(t,r,n,e)},y:Nv,Y:Ev,Z:kv,"%":Iv};function w(t,n){return function(e){var r,i,o,a=[],u=-1,c=0,f=t.length;for(e instanceof Date||(e=new Date(+e));++u53)return null;"w"in o||(o.w=1),"Z"in o?(i=(r=sv(lv(o.y,0,1))).getUTCDay(),r=i>4||0===i?qy.ceil(r):qy(r),r=yy.offset(r,7*(o.V-1)),o.y=r.getUTCFullYear(),o.m=r.getUTCMonth(),o.d=r.getUTCDate()+(o.w+6)%7):(i=(r=fv(lv(o.y,0,1))).getDay(),r=i>4||0===i?wy.ceil(r):wy(r),r=py.offset(r,7*(o.V-1)),o.y=r.getFullYear(),o.m=r.getMonth(),o.d=r.getDate()+(o.w+6)%7)}else("W"in o||"U"in o)&&("w"in o||(o.w="u"in o?o.u%7:"W"in o?1:0),i="Z"in o?sv(lv(o.y,0,1)).getUTCDay():fv(lv(o.y,0,1)).getDay(),o.m=0,o.d="W"in o?(o.w+6)%7+7*o.W-(i+5)%7:o.w+7*o.U-(i+6)%7);return"Z"in o?(o.H+=o.Z/100|0,o.M+=o.Z%100,sv(o)):fv(o)}}function T(t,n,e,r){for(var i,o,a=0,u=n.length,c=e.length;a=c)return-1;if(37===(i=n.charCodeAt(a++))){if(i=n.charAt(a++),!(o=x[i in pv?n.charAt(a++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}return b.x=w(e,b),b.X=w(r,b),b.c=w(n,b),m.x=w(e,m),m.X=w(r,m),m.c=w(n,m),{format:function(t){var n=w(t+="",b);return n.toString=function(){return t},n},parse:function(t){var n=M(t+="",!1);return n.toString=function(){return t},n},utcFormat:function(t){var n=w(t+="",m);return n.toString=function(){return t},n},utcParse:function(t){var n=M(t+="",!0);return n.toString=function(){return t},n}}}var dv,pv={"-":"",_:" ",0:"0"},gv=/^\s*\d+/,yv=/^%/,vv=/[\\^$*+?|[\]().{}]/g;function _v(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o[t.toLowerCase(),n])))}function wv(t,n,e){var r=gv.exec(n.slice(e,e+1));return r?(t.w=+r[0],e+r[0].length):-1}function Mv(t,n,e){var r=gv.exec(n.slice(e,e+1));return r?(t.u=+r[0],e+r[0].length):-1}function Tv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.U=+r[0],e+r[0].length):-1}function Av(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.V=+r[0],e+r[0].length):-1}function Sv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.W=+r[0],e+r[0].length):-1}function Ev(t,n,e){var r=gv.exec(n.slice(e,e+4));return r?(t.y=+r[0],e+r[0].length):-1}function Nv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.y=+r[0]+(+r[0]>68?1900:2e3),e+r[0].length):-1}function kv(t,n,e){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function Cv(t,n,e){var r=gv.exec(n.slice(e,e+1));return r?(t.q=3*r[0]-3,e+r[0].length):-1}function Pv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function zv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function $v(t,n,e){var r=gv.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function Dv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function Rv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function Fv(t,n,e){var r=gv.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function qv(t,n,e){var r=gv.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function Uv(t,n,e){var r=gv.exec(n.slice(e,e+6));return r?(t.L=Math.floor(r[0]/1e3),e+r[0].length):-1}function Iv(t,n,e){var r=yv.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function Ov(t,n,e){var r=gv.exec(n.slice(e));return r?(t.Q=+r[0],e+r[0].length):-1}function Bv(t,n,e){var r=gv.exec(n.slice(e));return r?(t.s=+r[0],e+r[0].length):-1}function Yv(t,n){return _v(t.getDate(),n,2)}function Lv(t,n){return _v(t.getHours(),n,2)}function jv(t,n){return _v(t.getHours()%12||12,n,2)}function Hv(t,n){return _v(1+py.count(tv(t),t),n,3)}function Xv(t,n){return _v(t.getMilliseconds(),n,3)}function Gv(t,n){return Xv(t,n)+"000"}function Vv(t,n){return _v(t.getMonth()+1,n,2)}function Wv(t,n){return _v(t.getMinutes(),n,2)}function Zv(t,n){return _v(t.getSeconds(),n,2)}function Kv(t){var n=t.getDay();return 0===n?7:n}function Qv(t,n){return _v(xy.count(tv(t)-1,t),n,2)}function Jv(t){var n=t.getDay();return n>=4||0===n?Ay(t):Ay.ceil(t)}function t_(t,n){return t=Jv(t),_v(Ay.count(tv(t),t)+(4===tv(t).getDay()),n,2)}function n_(t){return t.getDay()}function e_(t,n){return _v(wy.count(tv(t)-1,t),n,2)}function r_(t,n){return _v(t.getFullYear()%100,n,2)}function i_(t,n){return _v((t=Jv(t)).getFullYear()%100,n,2)}function o_(t,n){return _v(t.getFullYear()%1e4,n,4)}function a_(t,n){var e=t.getDay();return _v((t=e>=4||0===e?Ay(t):Ay.ceil(t)).getFullYear()%1e4,n,4)}function u_(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+_v(n/60|0,"0",2)+_v(n%60,"0",2)}function c_(t,n){return _v(t.getUTCDate(),n,2)}function f_(t,n){return _v(t.getUTCHours(),n,2)}function s_(t,n){return _v(t.getUTCHours()%12||12,n,2)}function l_(t,n){return _v(1+yy.count(ev(t),t),n,3)}function h_(t,n){return _v(t.getUTCMilliseconds(),n,3)}function d_(t,n){return h_(t,n)+"000"}function p_(t,n){return _v(t.getUTCMonth()+1,n,2)}function g_(t,n){return _v(t.getUTCMinutes(),n,2)}function y_(t,n){return _v(t.getUTCSeconds(),n,2)}function v_(t){var n=t.getUTCDay();return 0===n?7:n}function __(t,n){return _v(Fy.count(ev(t)-1,t),n,2)}function b_(t){var n=t.getUTCDay();return n>=4||0===n?Oy(t):Oy.ceil(t)}function m_(t,n){return t=b_(t),_v(Oy.count(ev(t),t)+(4===ev(t).getUTCDay()),n,2)}function x_(t){return t.getUTCDay()}function w_(t,n){return _v(qy.count(ev(t)-1,t),n,2)}function M_(t,n){return _v(t.getUTCFullYear()%100,n,2)}function T_(t,n){return _v((t=b_(t)).getUTCFullYear()%100,n,2)}function A_(t,n){return _v(t.getUTCFullYear()%1e4,n,4)}function S_(t,n){var e=t.getUTCDay();return _v((t=e>=4||0===e?Oy(t):Oy.ceil(t)).getUTCFullYear()%1e4,n,4)}function E_(){return"+0000"}function N_(){return"%"}function k_(t){return+t}function C_(t){return Math.floor(+t/1e3)}function P_(n){return dv=hv(n),t.timeFormat=dv.format,t.timeParse=dv.parse,t.utcFormat=dv.utcFormat,t.utcParse=dv.utcParse,dv}t.timeFormat=void 0,t.timeParse=void 0,t.utcFormat=void 0,t.utcParse=void 0,P_({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var z_="%Y-%m-%dT%H:%M:%S.%LZ";var $_=Date.prototype.toISOString?function(t){return t.toISOString()}:t.utcFormat(z_),D_=$_;var R_=+new Date("2000-01-01T00:00:00.000Z")?function(t){var n=new Date(t);return isNaN(n)?null:n}:t.utcParse(z_),F_=R_;function q_(t){return new Date(t)}function U_(t){return t instanceof Date?+t:+new Date(+t)}function I_(t,n,e,r,i,o,a,u,c,f){var s=Sg(),l=s.invert,h=s.domain,d=f(".%L"),p=f(":%S"),g=f("%I:%M"),y=f("%I %p"),v=f("%a %d"),_=f("%b %d"),b=f("%B"),m=f("%Y");function x(t){return(c(t)Fr(t[t.length-1]),ib=new Array(3).concat("d8b365f5f5f55ab4ac","a6611adfc27d80cdc1018571","a6611adfc27df5f5f580cdc1018571","8c510ad8b365f6e8c3c7eae55ab4ac01665e","8c510ad8b365f6e8c3f5f5f5c7eae55ab4ac01665e","8c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e","8c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e","5430058c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e003c30","5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30").map(H_),ob=rb(ib),ab=new Array(3).concat("af8dc3f7f7f77fbf7b","7b3294c2a5cfa6dba0008837","7b3294c2a5cff7f7f7a6dba0008837","762a83af8dc3e7d4e8d9f0d37fbf7b1b7837","762a83af8dc3e7d4e8f7f7f7d9f0d37fbf7b1b7837","762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b7837","762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b7837","40004b762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b783700441b","40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b").map(H_),ub=rb(ab),cb=new Array(3).concat("e9a3c9f7f7f7a1d76a","d01c8bf1b6dab8e1864dac26","d01c8bf1b6daf7f7f7b8e1864dac26","c51b7de9a3c9fde0efe6f5d0a1d76a4d9221","c51b7de9a3c9fde0eff7f7f7e6f5d0a1d76a4d9221","c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221","c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221","8e0152c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221276419","8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419").map(H_),fb=rb(cb),sb=new Array(3).concat("998ec3f7f7f7f1a340","5e3c99b2abd2fdb863e66101","5e3c99b2abd2f7f7f7fdb863e66101","542788998ec3d8daebfee0b6f1a340b35806","542788998ec3d8daebf7f7f7fee0b6f1a340b35806","5427888073acb2abd2d8daebfee0b6fdb863e08214b35806","5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b35806","2d004b5427888073acb2abd2d8daebfee0b6fdb863e08214b358067f3b08","2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08").map(H_),lb=rb(sb),hb=new Array(3).concat("ef8a62f7f7f767a9cf","ca0020f4a58292c5de0571b0","ca0020f4a582f7f7f792c5de0571b0","b2182bef8a62fddbc7d1e5f067a9cf2166ac","b2182bef8a62fddbc7f7f7f7d1e5f067a9cf2166ac","b2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac","b2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac","67001fb2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac053061","67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061").map(H_),db=rb(hb),pb=new Array(3).concat("ef8a62ffffff999999","ca0020f4a582bababa404040","ca0020f4a582ffffffbababa404040","b2182bef8a62fddbc7e0e0e09999994d4d4d","b2182bef8a62fddbc7ffffffe0e0e09999994d4d4d","b2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d","b2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d","67001fb2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d1a1a1a","67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a").map(H_),gb=rb(pb),yb=new Array(3).concat("fc8d59ffffbf91bfdb","d7191cfdae61abd9e92c7bb6","d7191cfdae61ffffbfabd9e92c7bb6","d73027fc8d59fee090e0f3f891bfdb4575b4","d73027fc8d59fee090ffffbfe0f3f891bfdb4575b4","d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4","d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4","a50026d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4313695","a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695").map(H_),vb=rb(yb),_b=new Array(3).concat("fc8d59ffffbf91cf60","d7191cfdae61a6d96a1a9641","d7191cfdae61ffffbfa6d96a1a9641","d73027fc8d59fee08bd9ef8b91cf601a9850","d73027fc8d59fee08bffffbfd9ef8b91cf601a9850","d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850","d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850","a50026d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850006837","a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837").map(H_),bb=rb(_b),mb=new Array(3).concat("fc8d59ffffbf99d594","d7191cfdae61abdda42b83ba","d7191cfdae61ffffbfabdda42b83ba","d53e4ffc8d59fee08be6f59899d5943288bd","d53e4ffc8d59fee08bffffbfe6f59899d5943288bd","d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd","d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd","9e0142d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd5e4fa2","9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2").map(H_),xb=rb(mb),wb=new Array(3).concat("e5f5f999d8c92ca25f","edf8fbb2e2e266c2a4238b45","edf8fbb2e2e266c2a42ca25f006d2c","edf8fbccece699d8c966c2a42ca25f006d2c","edf8fbccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45006d2c00441b").map(H_),Mb=rb(wb),Tb=new Array(3).concat("e0ecf49ebcda8856a7","edf8fbb3cde38c96c688419d","edf8fbb3cde38c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d810f7c4d004b").map(H_),Ab=rb(Tb),Sb=new Array(3).concat("e0f3dba8ddb543a2ca","f0f9e8bae4bc7bccc42b8cbe","f0f9e8bae4bc7bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe0868ac084081").map(H_),Eb=rb(Sb),Nb=new Array(3).concat("fee8c8fdbb84e34a33","fef0d9fdcc8afc8d59d7301f","fef0d9fdcc8afc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301fb300007f0000").map(H_),kb=rb(Nb),Cb=new Array(3).concat("ece2f0a6bddb1c9099","f6eff7bdc9e167a9cf02818a","f6eff7bdc9e167a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016c59014636").map(H_),Pb=rb(Cb),zb=new Array(3).concat("ece7f2a6bddb2b8cbe","f1eef6bdc9e174a9cf0570b0","f1eef6bdc9e174a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0045a8d023858").map(H_),$b=rb(zb),Db=new Array(3).concat("e7e1efc994c7dd1c77","f1eef6d7b5d8df65b0ce1256","f1eef6d7b5d8df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125698004367001f").map(H_),Rb=rb(Db),Fb=new Array(3).concat("fde0ddfa9fb5c51b8a","feebe2fbb4b9f768a1ae017e","feebe2fbb4b9f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a017749006a").map(H_),qb=rb(Fb),Ub=new Array(3).concat("edf8b17fcdbb2c7fb8","ffffcca1dab441b6c4225ea8","ffffcca1dab441b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea8253494081d58").map(H_),Ib=rb(Ub),Ob=new Array(3).concat("f7fcb9addd8e31a354","ffffccc2e69978c679238443","ffffccc2e69978c67931a354006837","ffffccd9f0a3addd8e78c67931a354006837","ffffccd9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443006837004529").map(H_),Bb=rb(Ob),Yb=new Array(3).concat("fff7bcfec44fd95f0e","ffffd4fed98efe9929cc4c02","ffffd4fed98efe9929d95f0e993404","ffffd4fee391fec44ffe9929d95f0e993404","ffffd4fee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c02993404662506").map(H_),Lb=rb(Yb),jb=new Array(3).concat("ffeda0feb24cf03b20","ffffb2fecc5cfd8d3ce31a1c","ffffb2fecc5cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cbd0026800026").map(H_),Hb=rb(jb),Xb=new Array(3).concat("deebf79ecae13182bd","eff3ffbdd7e76baed62171b5","eff3ffbdd7e76baed63182bd08519c","eff3ffc6dbef9ecae16baed63182bd08519c","eff3ffc6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b508519c08306b").map(H_),Gb=rb(Xb),Vb=new Array(3).concat("e5f5e0a1d99b31a354","edf8e9bae4b374c476238b45","edf8e9bae4b374c47631a354006d2c","edf8e9c7e9c0a1d99b74c47631a354006d2c","edf8e9c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45006d2c00441b").map(H_),Wb=rb(Vb),Zb=new Array(3).concat("f0f0f0bdbdbd636363","f7f7f7cccccc969696525252","f7f7f7cccccc969696636363252525","f7f7f7d9d9d9bdbdbd969696636363252525","f7f7f7d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525000000").map(H_),Kb=rb(Zb),Qb=new Array(3).concat("efedf5bcbddc756bb1","f2f0f7cbc9e29e9ac86a51a3","f2f0f7cbc9e29e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a354278f3f007d").map(H_),Jb=rb(Qb),tm=new Array(3).concat("fee0d2fc9272de2d26","fee5d9fcae91fb6a4acb181d","fee5d9fcae91fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181da50f1567000d").map(H_),nm=rb(tm),em=new Array(3).concat("fee6cefdae6be6550d","feeddefdbe85fd8d3cd94701","feeddefdbe85fd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d94801a636037f2704").map(H_),rm=rb(em);var im=hi(Tr(300,.5,0),Tr(-240,.5,1)),om=hi(Tr(-100,.75,.35),Tr(80,1.5,.8)),am=hi(Tr(260,.75,.35),Tr(80,1.5,.8)),um=Tr();var cm=Fe(),fm=Math.PI/3,sm=2*Math.PI/3;function lm(t){var n=t.length;return function(e){return t[Math.max(0,Math.min(n-1,Math.floor(e*n)))]}}var hm=lm(H_("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725")),dm=lm(H_("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),pm=lm(H_("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),gm=lm(H_("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921"));function ym(t){return function(){return t}}const vm=Math.abs,_m=Math.atan2,bm=Math.cos,mm=Math.max,xm=Math.min,wm=Math.sin,Mm=Math.sqrt,Tm=1e-12,Am=Math.PI,Sm=Am/2,Em=2*Am;function Nm(t){return t>=1?Sm:t<=-1?-Sm:Math.asin(t)}function km(t){let n=3;return t.digits=function(e){if(!arguments.length)return n;if(null==e)n=null;else{const t=Math.floor(e);if(!(t>=0))throw new RangeError(`invalid digits: ${e}`);n=t}return t},()=>new Ua(n)}function Cm(t){return t.innerRadius}function Pm(t){return t.outerRadius}function zm(t){return t.startAngle}function $m(t){return t.endAngle}function Dm(t){return t&&t.padAngle}function Rm(t,n,e,r,i,o,a){var u=t-e,c=n-r,f=(a?o:-o)/Mm(u*u+c*c),s=f*c,l=-f*u,h=t+s,d=n+l,p=e+s,g=r+l,y=(h+p)/2,v=(d+g)/2,_=p-h,b=g-d,m=_*_+b*b,x=i-o,w=h*g-p*d,M=(b<0?-1:1)*Mm(mm(0,x*x*m-w*w)),T=(w*b-_*M)/m,A=(-w*_-b*M)/m,S=(w*b+_*M)/m,E=(-w*_+b*M)/m,N=T-y,k=A-v,C=S-y,P=E-v;return N*N+k*k>C*C+P*P&&(T=S,A=E),{cx:T,cy:A,x01:-s,y01:-l,x11:T*(i/x-1),y11:A*(i/x-1)}}var Fm=Array.prototype.slice;function qm(t){return"object"==typeof t&&"length"in t?t:Array.from(t)}function Um(t){this._context=t}function Im(t){return new Um(t)}function Om(t){return t[0]}function Bm(t){return t[1]}function Ym(t,n){var e=ym(!0),r=null,i=Im,o=null,a=km(u);function u(u){var c,f,s,l=(u=qm(u)).length,h=!1;for(null==r&&(o=i(s=a())),c=0;c<=l;++c)!(c=l;--h)u.point(v[h],_[h]);u.lineEnd(),u.areaEnd()}y&&(v[s]=+t(d,s,f),_[s]=+n(d,s,f),u.point(r?+r(d,s,f):v[s],e?+e(d,s,f):_[s]))}if(p)return u=null,p+""||null}function s(){return Ym().defined(i).curve(a).context(o)}return t="function"==typeof t?t:void 0===t?Om:ym(+t),n="function"==typeof n?n:ym(void 0===n?0:+n),e="function"==typeof e?e:void 0===e?Bm:ym(+e),f.x=function(n){return arguments.length?(t="function"==typeof n?n:ym(+n),r=null,f):t},f.x0=function(n){return arguments.length?(t="function"==typeof n?n:ym(+n),f):t},f.x1=function(t){return arguments.length?(r=null==t?null:"function"==typeof t?t:ym(+t),f):r},f.y=function(t){return arguments.length?(n="function"==typeof t?t:ym(+t),e=null,f):n},f.y0=function(t){return arguments.length?(n="function"==typeof t?t:ym(+t),f):n},f.y1=function(t){return arguments.length?(e=null==t?null:"function"==typeof t?t:ym(+t),f):e},f.lineX0=f.lineY0=function(){return s().x(t).y(n)},f.lineY1=function(){return s().x(t).y(e)},f.lineX1=function(){return s().x(r).y(n)},f.defined=function(t){return arguments.length?(i="function"==typeof t?t:ym(!!t),f):i},f.curve=function(t){return arguments.length?(a=t,null!=o&&(u=a(o)),f):a},f.context=function(t){return arguments.length?(null==t?o=u=null:u=a(o=t),f):o},f}function jm(t,n){return nt?1:n>=t?0:NaN}function Hm(t){return t}Um.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var Xm=Vm(Im);function Gm(t){this._curve=t}function Vm(t){function n(n){return new Gm(t(n))}return n._curve=t,n}function Wm(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(Vm(t)):n()._curve},t}function Zm(){return Wm(Ym().curve(Xm))}function Km(){var t=Lm().curve(Xm),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return Wm(e())},delete t.lineX0,t.lineEndAngle=function(){return Wm(r())},delete t.lineX1,t.lineInnerRadius=function(){return Wm(i())},delete t.lineY0,t.lineOuterRadius=function(){return Wm(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(Vm(t)):n()._curve},t}function Qm(t,n){return[(n=+n)*Math.cos(t-=Math.PI/2),n*Math.sin(t)]}Gm.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};class Jm{constructor(t,n){this._context=t,this._x=n}areaStart(){this._line=0}areaEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line}point(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._x?this._context.bezierCurveTo(this._x0=(this._x0+t)/2,this._y0,this._x0,n,t,n):this._context.bezierCurveTo(this._x0,this._y0=(this._y0+n)/2,t,this._y0,t,n)}this._x0=t,this._y0=n}}class tx{constructor(t){this._context=t}lineStart(){this._point=0}lineEnd(){}point(t,n){if(t=+t,n=+n,0===this._point)this._point=1;else{const e=Qm(this._x0,this._y0),r=Qm(this._x0,this._y0=(this._y0+n)/2),i=Qm(t,this._y0),o=Qm(t,n);this._context.moveTo(...e),this._context.bezierCurveTo(...r,...i,...o)}this._x0=t,this._y0=n}}function nx(t){return new Jm(t,!0)}function ex(t){return new Jm(t,!1)}function rx(t){return new tx(t)}function ix(t){return t.source}function ox(t){return t.target}function ax(t){let n=ix,e=ox,r=Om,i=Bm,o=null,a=null,u=km(c);function c(){let c;const f=Fm.call(arguments),s=n.apply(this,f),l=e.apply(this,f);if(null==o&&(a=t(c=u())),a.lineStart(),f[0]=s,a.point(+r.apply(this,f),+i.apply(this,f)),f[0]=l,a.point(+r.apply(this,f),+i.apply(this,f)),a.lineEnd(),c)return a=null,c+""||null}return c.source=function(t){return arguments.length?(n=t,c):n},c.target=function(t){return arguments.length?(e=t,c):e},c.x=function(t){return arguments.length?(r="function"==typeof t?t:ym(+t),c):r},c.y=function(t){return arguments.length?(i="function"==typeof t?t:ym(+t),c):i},c.context=function(n){return arguments.length?(null==n?o=a=null:a=t(o=n),c):o},c}const ux=Mm(3);var cx={draw(t,n){const e=.59436*Mm(n+xm(n/28,.75)),r=e/2,i=r*ux;t.moveTo(0,e),t.lineTo(0,-e),t.moveTo(-i,-r),t.lineTo(i,r),t.moveTo(-i,r),t.lineTo(i,-r)}},fx={draw(t,n){const e=Mm(n/Am);t.moveTo(e,0),t.arc(0,0,e,0,Em)}},sx={draw(t,n){const e=Mm(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}};const lx=Mm(1/3),hx=2*lx;var dx={draw(t,n){const e=Mm(n/hx),r=e*lx;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},px={draw(t,n){const e=.62625*Mm(n);t.moveTo(0,-e),t.lineTo(e,0),t.lineTo(0,e),t.lineTo(-e,0),t.closePath()}},gx={draw(t,n){const e=.87559*Mm(n-xm(n/7,2));t.moveTo(-e,0),t.lineTo(e,0),t.moveTo(0,e),t.lineTo(0,-e)}},yx={draw(t,n){const e=Mm(n),r=-e/2;t.rect(r,r,e,e)}},vx={draw(t,n){const e=.4431*Mm(n);t.moveTo(e,e),t.lineTo(e,-e),t.lineTo(-e,-e),t.lineTo(-e,e),t.closePath()}};const _x=wm(Am/10)/wm(7*Am/10),bx=wm(Em/10)*_x,mx=-bm(Em/10)*_x;var xx={draw(t,n){const e=Mm(.8908130915292852*n),r=bx*e,i=mx*e;t.moveTo(0,-e),t.lineTo(r,i);for(let n=1;n<5;++n){const o=Em*n/5,a=bm(o),u=wm(o);t.lineTo(u*e,-a*e),t.lineTo(a*r-u*i,u*r+a*i)}t.closePath()}};const wx=Mm(3);var Mx={draw(t,n){const e=-Mm(n/(3*wx));t.moveTo(0,2*e),t.lineTo(-wx*e,-e),t.lineTo(wx*e,-e),t.closePath()}};const Tx=Mm(3);var Ax={draw(t,n){const e=.6824*Mm(n),r=e/2,i=e*Tx/2;t.moveTo(0,-e),t.lineTo(i,r),t.lineTo(-i,r),t.closePath()}};const Sx=-.5,Ex=Mm(3)/2,Nx=1/Mm(12),kx=3*(Nx/2+1);var Cx={draw(t,n){const e=Mm(n/kx),r=e/2,i=e*Nx,o=r,a=e*Nx+e,u=-o,c=a;t.moveTo(r,i),t.lineTo(o,a),t.lineTo(u,c),t.lineTo(Sx*r-Ex*i,Ex*r+Sx*i),t.lineTo(Sx*o-Ex*a,Ex*o+Sx*a),t.lineTo(Sx*u-Ex*c,Ex*u+Sx*c),t.lineTo(Sx*r+Ex*i,Sx*i-Ex*r),t.lineTo(Sx*o+Ex*a,Sx*a-Ex*o),t.lineTo(Sx*u+Ex*c,Sx*c-Ex*u),t.closePath()}},Px={draw(t,n){const e=.6189*Mm(n-xm(n/6,1.7));t.moveTo(-e,-e),t.lineTo(e,e),t.moveTo(-e,e),t.lineTo(e,-e)}};const zx=[fx,sx,dx,yx,xx,Mx,Cx],$x=[fx,gx,Px,Ax,cx,vx,px];function Dx(){}function Rx(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function Fx(t){this._context=t}function qx(t){this._context=t}function Ux(t){this._context=t}function Ix(t,n){this._basis=new Fx(t),this._beta=n}Fx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:Rx(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:Rx(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},qx.prototype={areaStart:Dx,areaEnd:Dx,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:Rx(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Ux.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:Rx(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Ix.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],a=t[e]-i,u=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*a),this._beta*n[c]+(1-this._beta)*(o+r*u));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var Ox=function t(n){function e(t){return 1===n?new Fx(t):new Ix(t,n)}return e.beta=function(n){return t(+n)},e}(.85);function Bx(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function Yx(t,n){this._context=t,this._k=(1-n)/6}Yx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:Bx(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:Bx(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Lx=function t(n){function e(t){return new Yx(t,n)}return e.tension=function(n){return t(+n)},e}(0);function jx(t,n){this._context=t,this._k=(1-n)/6}jx.prototype={areaStart:Dx,areaEnd:Dx,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Bx(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Hx=function t(n){function e(t){return new jx(t,n)}return e.tension=function(n){return t(+n)},e}(0);function Xx(t,n){this._context=t,this._k=(1-n)/6}Xx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Bx(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Gx=function t(n){function e(t){return new Xx(t,n)}return e.tension=function(n){return t(+n)},e}(0);function Vx(t,n,e){var r=t._x1,i=t._y1,o=t._x2,a=t._y2;if(t._l01_a>Tm){var u=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*u-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*u-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>Tm){var f=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,s=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*f+t._x1*t._l23_2a-n*t._l12_2a)/s,a=(a*f+t._y1*t._l23_2a-e*t._l12_2a)/s}t._context.bezierCurveTo(r,i,o,a,t._x2,t._y2)}function Wx(t,n){this._context=t,this._alpha=n}Wx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:Vx(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Zx=function t(n){function e(t){return n?new Wx(t,n):new Yx(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function Kx(t,n){this._context=t,this._alpha=n}Kx.prototype={areaStart:Dx,areaEnd:Dx,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Vx(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Qx=function t(n){function e(t){return n?new Kx(t,n):new jx(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function Jx(t,n){this._context=t,this._alpha=n}Jx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Vx(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var tw=function t(n){function e(t){return n?new Jx(t,n):new Xx(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function nw(t){this._context=t}function ew(t){return t<0?-1:1}function rw(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),a=(e-t._y1)/(i||r<0&&-0),u=(o*i+a*r)/(r+i);return(ew(o)+ew(a))*Math.min(Math.abs(o),Math.abs(a),.5*Math.abs(u))||0}function iw(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function ow(t,n,e){var r=t._x0,i=t._y0,o=t._x1,a=t._y1,u=(o-r)/3;t._context.bezierCurveTo(r+u,i+u*n,o-u,a-u*e,o,a)}function aw(t){this._context=t}function uw(t){this._context=new cw(t)}function cw(t){this._context=t}function fw(t){this._context=t}function sw(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),a=new Array(r);for(i[0]=0,o[0]=2,a[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(a[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n1)for(var e,r,i,o=1,a=t[n[0]],u=a.length;o=0;)e[n]=n;return e}function pw(t,n){return t[n]}function gw(t){const n=[];return n.key=t,n}function yw(t){var n=t.map(vw);return dw(t).sort((function(t,e){return n[t]-n[e]}))}function vw(t){for(var n,e=-1,r=0,i=t.length,o=-1/0;++eo&&(o=n,r=e);return r}function _w(t){var n=t.map(bw);return dw(t).sort((function(t,e){return n[t]-n[e]}))}function bw(t){for(var n,e=0,r=-1,i=t.length;++r=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}};var mw=t=>()=>t;function xw(t,{sourceEvent:n,target:e,transform:r,dispatch:i}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},target:{value:e,enumerable:!0,configurable:!0},transform:{value:r,enumerable:!0,configurable:!0},_:{value:i}})}function ww(t,n,e){this.k=t,this.x=n,this.y=e}ww.prototype={constructor:ww,scale:function(t){return 1===t?this:new ww(this.k*t,this.x,this.y)},translate:function(t,n){return 0===t&0===n?this:new ww(this.k,this.x+this.k*t,this.y+this.k*n)},apply:function(t){return[t[0]*this.k+this.x,t[1]*this.k+this.y]},applyX:function(t){return t*this.k+this.x},applyY:function(t){return t*this.k+this.y},invert:function(t){return[(t[0]-this.x)/this.k,(t[1]-this.y)/this.k]},invertX:function(t){return(t-this.x)/this.k},invertY:function(t){return(t-this.y)/this.k},rescaleX:function(t){return t.copy().domain(t.range().map(this.invertX,this).map(t.invert,t))},rescaleY:function(t){return t.copy().domain(t.range().map(this.invertY,this).map(t.invert,t))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Mw=new ww(1,0,0);function Tw(t){for(;!t.__zoom;)if(!(t=t.parentNode))return Mw;return t.__zoom}function Aw(t){t.stopImmediatePropagation()}function Sw(t){t.preventDefault(),t.stopImmediatePropagation()}function Ew(t){return!(t.ctrlKey&&"wheel"!==t.type||t.button)}function Nw(){var t=this;return t instanceof SVGElement?(t=t.ownerSVGElement||t).hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]:[[0,0],[t.clientWidth,t.clientHeight]]}function kw(){return this.__zoom||Mw}function Cw(t){return-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002)*(t.ctrlKey?10:1)}function Pw(){return navigator.maxTouchPoints||"ontouchstart"in this}function zw(t,n,e){var r=t.invertX(n[0][0])-e[0][0],i=t.invertX(n[1][0])-e[1][0],o=t.invertY(n[0][1])-e[0][1],a=t.invertY(n[1][1])-e[1][1];return t.translate(i>r?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}Tw.prototype=ww.prototype,t.Adder=T,t.Delaunay=Lu,t.FormatSpecifier=tf,t.InternMap=InternMap,t.InternSet=InternSet,t.Node=Qd,t.Path=Ua,t.Voronoi=qu,t.ZoomTransform=ww,t.active=function(t,n){var e,r,i=t.__transition;if(i)for(r in n=null==n?null:n+"",i)if((e=i[r]).state>qi&&e.name===n)return new po([[t]],Zo,n,+r);return null},t.arc=function(){var t=Cm,n=Pm,e=ym(0),r=null,i=zm,o=$m,a=Dm,u=null,c=km(f);function f(){var f,s,l=+t.apply(this,arguments),h=+n.apply(this,arguments),d=i.apply(this,arguments)-Sm,p=o.apply(this,arguments)-Sm,g=vm(p-d),y=p>d;if(u||(u=f=c()),hTm)if(g>Em-Tm)u.moveTo(h*bm(d),h*wm(d)),u.arc(0,0,h,d,p,!y),l>Tm&&(u.moveTo(l*bm(p),l*wm(p)),u.arc(0,0,l,p,d,y));else{var v,_,b=d,m=p,x=d,w=p,M=g,T=g,A=a.apply(this,arguments)/2,S=A>Tm&&(r?+r.apply(this,arguments):Mm(l*l+h*h)),E=xm(vm(h-l)/2,+e.apply(this,arguments)),N=E,k=E;if(S>Tm){var C=Nm(S/l*wm(A)),P=Nm(S/h*wm(A));(M-=2*C)>Tm?(x+=C*=y?1:-1,w-=C):(M=0,x=w=(d+p)/2),(T-=2*P)>Tm?(b+=P*=y?1:-1,m-=P):(T=0,b=m=(d+p)/2)}var z=h*bm(b),$=h*wm(b),D=l*bm(w),R=l*wm(w);if(E>Tm){var F,q=h*bm(m),U=h*wm(m),I=l*bm(x),O=l*wm(x);if(g1?0:t<-1?Am:Math.acos(t)}((B*L+Y*j)/(Mm(B*B+Y*Y)*Mm(L*L+j*j)))/2),X=Mm(F[0]*F[0]+F[1]*F[1]);N=xm(E,(l-X)/(H-1)),k=xm(E,(h-X)/(H+1))}else N=k=0}T>Tm?k>Tm?(v=Rm(I,O,z,$,h,k,y),_=Rm(q,U,D,R,h,k,y),u.moveTo(v.cx+v.x01,v.cy+v.y01),kTm&&M>Tm?N>Tm?(v=Rm(D,R,q,U,l,-N,y),_=Rm(z,$,I,O,l,-N,y),u.lineTo(v.cx+v.x01,v.cy+v.y01),N=0))throw new RangeError("invalid r");let e=t.length;if(!((e=Math.floor(e))>=0))throw new RangeError("invalid length");if(!e||!n)return t;const r=y(n),i=t.slice();return r(t,i,0,e,1),r(i,t,0,e,1),r(t,i,0,e,1),t},t.blur2=l,t.blurImage=h,t.brush=function(){return wa(la)},t.brushSelection=function(t){var n=t.__brush;return n?n.dim.output(n.selection):null},t.brushX=function(){return wa(fa)},t.brushY=function(){return wa(sa)},t.buffer=function(t,n){return fetch(t,n).then(_c)},t.chord=function(){return za(!1,!1)},t.chordDirected=function(){return za(!0,!1)},t.chordTranspose=function(){return za(!1,!0)},t.cluster=function(){var t=Ld,n=1,e=1,r=!1;function i(i){var o,a=0;i.eachAfter((function(n){var e=n.children;e?(n.x=function(t){return t.reduce(jd,0)/t.length}(e),n.y=function(t){return 1+t.reduce(Hd,0)}(e)):(n.x=o?a+=t(n,o):0,n.y=0,o=n)}));var u=function(t){for(var n;n=t.children;)t=n[0];return t}(i),c=function(t){for(var n;n=t.children;)t=n[n.length-1];return t}(i),f=u.x-t(u,c)/2,s=c.x+t(c,u)/2;return i.eachAfter(r?function(t){t.x=(t.x-i.x)*n,t.y=(i.y-t.y)*e}:function(t){t.x=(t.x-f)/(s-f)*n,t.y=(1-(i.y?t.y/i.y:1))*e})}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.color=ze,t.contourDensity=function(){var t=fu,n=su,e=lu,r=960,i=500,o=20,a=2,u=3*o,c=r+2*u>>a,f=i+2*u>>a,s=Qa(20);function h(r){var i=new Float32Array(c*f),s=Math.pow(2,-a),h=-1;for(const o of r){var d=(t(o,++h,r)+u)*s,p=(n(o,h,r)+u)*s,g=+e(o,h,r);if(g&&d>=0&&d=0&&pt*r)))(n).map(((t,n)=>(t.value=+e[n],p(t))))}function p(t){return t.coordinates.forEach(g),t}function g(t){t.forEach(y)}function y(t){t.forEach(v)}function v(t){t[0]=t[0]*Math.pow(2,a)-u,t[1]=t[1]*Math.pow(2,a)-u}function _(){return c=r+2*(u=3*o)>>a,f=i+2*u>>a,d}return d.contours=function(t){var n=h(t),e=iu().size([c,f]),r=Math.pow(2,2*a),i=t=>{t=+t;var i=p(e.contour(n,t*r));return i.value=t,i};return Object.defineProperty(i,"max",{get:()=>J(n)/r}),i},d.x=function(n){return arguments.length?(t="function"==typeof n?n:Qa(+n),d):t},d.y=function(t){return arguments.length?(n="function"==typeof t?t:Qa(+t),d):n},d.weight=function(t){return arguments.length?(e="function"==typeof t?t:Qa(+t),d):e},d.size=function(t){if(!arguments.length)return[r,i];var n=+t[0],e=+t[1];if(!(n>=0&&e>=0))throw new Error("invalid size");return r=n,i=e,_()},d.cellSize=function(t){if(!arguments.length)return 1<=1))throw new Error("invalid cell size");return a=Math.floor(Math.log(t)/Math.LN2),_()},d.thresholds=function(t){return arguments.length?(s="function"==typeof t?t:Array.isArray(t)?Qa(Za.call(t)):Qa(t),d):s},d.bandwidth=function(t){if(!arguments.length)return Math.sqrt(o*(o+1));if(!((t=+t)>=0))throw new Error("invalid bandwidth");return o=(Math.sqrt(4*t*t+1)-1)/2,_()},d},t.contours=iu,t.count=v,t.create=function(t){return Zn(Yt(t).call(document.documentElement))},t.creator=Yt,t.cross=function(...t){const n="function"==typeof t[t.length-1]&&function(t){return n=>t(...n)}(t.pop()),e=(t=t.map(m)).map(_),r=t.length-1,i=new Array(r+1).fill(0),o=[];if(r<0||e.some(b))return o;for(;;){o.push(i.map(((n,e)=>t[e][n])));let a=r;for(;++i[a]===e[a];){if(0===a)return n?o.map(n):o;i[a--]=0}}},t.csv=wc,t.csvFormat=rc,t.csvFormatBody=ic,t.csvFormatRow=ac,t.csvFormatRows=oc,t.csvFormatValue=uc,t.csvParse=nc,t.csvParseRows=ec,t.cubehelix=Tr,t.cumsum=function(t,n){var e=0,r=0;return Float64Array.from(t,void 0===n?t=>e+=+t||0:i=>e+=+n(i,r++,t)||0)},t.curveBasis=function(t){return new Fx(t)},t.curveBasisClosed=function(t){return new qx(t)},t.curveBasisOpen=function(t){return new Ux(t)},t.curveBumpX=nx,t.curveBumpY=ex,t.curveBundle=Ox,t.curveCardinal=Lx,t.curveCardinalClosed=Hx,t.curveCardinalOpen=Gx,t.curveCatmullRom=Zx,t.curveCatmullRomClosed=Qx,t.curveCatmullRomOpen=tw,t.curveLinear=Im,t.curveLinearClosed=function(t){return new nw(t)},t.curveMonotoneX=function(t){return new aw(t)},t.curveMonotoneY=function(t){return new uw(t)},t.curveNatural=function(t){return new fw(t)},t.curveStep=function(t){return new lw(t,.5)},t.curveStepAfter=function(t){return new lw(t,1)},t.curveStepBefore=function(t){return new lw(t,0)},t.descending=e,t.deviation=w,t.difference=function(t,...n){t=new InternSet(t);for(const e of n)for(const n of e)t.delete(n);return t},t.disjoint=function(t,n){const e=n[Symbol.iterator](),r=new InternSet;for(const n of t){if(r.has(n))return!1;let t,i;for(;({value:t,done:i}=e.next())&&!i;){if(Object.is(n,t))return!1;r.add(t)}}return!0},t.dispatch=$t,t.drag=function(){var t,n,e,r,i=se,o=le,a=he,u=de,c={},f=$t("start","drag","end"),s=0,l=0;function h(t){t.on("mousedown.drag",d).filter(u).on("touchstart.drag",y).on("touchmove.drag",v,ee).on("touchend.drag touchcancel.drag",_).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function d(a,u){if(!r&&i.call(this,a,u)){var c=b(this,o.call(this,a,u),a,u,"mouse");c&&(Zn(a.view).on("mousemove.drag",p,re).on("mouseup.drag",g,re),ae(a.view),ie(a),e=!1,t=a.clientX,n=a.clientY,c("start",a))}}function p(r){if(oe(r),!e){var i=r.clientX-t,o=r.clientY-n;e=i*i+o*o>l}c.mouse("drag",r)}function g(t){Zn(t.view).on("mousemove.drag mouseup.drag",null),ue(t.view,e),oe(t),c.mouse("end",t)}function y(t,n){if(i.call(this,t,n)){var e,r,a=t.changedTouches,u=o.call(this,t,n),c=a.length;for(e=0;e+t,t.easePoly=wo,t.easePolyIn=mo,t.easePolyInOut=wo,t.easePolyOut=xo,t.easeQuad=_o,t.easeQuadIn=function(t){return t*t},t.easeQuadInOut=_o,t.easeQuadOut=function(t){return t*(2-t)},t.easeSin=Ao,t.easeSinIn=function(t){return 1==+t?1:1-Math.cos(t*To)},t.easeSinInOut=Ao,t.easeSinOut=function(t){return Math.sin(t*To)},t.every=function(t,n){if("function"!=typeof n)throw new TypeError("test is not a function");let e=-1;for(const r of t)if(!n(r,++e,t))return!1;return!0},t.extent=M,t.fcumsum=function(t,n){const e=new T;let r=-1;return Float64Array.from(t,void 0===n?t=>e.add(+t||0):i=>e.add(+n(i,++r,t)||0))},t.filter=function(t,n){if("function"!=typeof n)throw new TypeError("test is not a function");const e=[];let r=-1;for(const i of t)n(i,++r,t)&&e.push(i);return e},t.flatGroup=function(t,...n){return z(P(t,...n),n)},t.flatRollup=function(t,n,...e){return z(D(t,n,...e),e)},t.forceCenter=function(t,n){var e,r=1;function i(){var i,o,a=e.length,u=0,c=0;for(i=0;if+p||os+p||ac.index){var g=f-u.x-u.vx,y=s-u.y-u.vy,v=g*g+y*y;vt.r&&(t.r=t[n].r)}function c(){if(n){var r,i,o=n.length;for(e=new Array(o),r=0;r[u(t,n,r),t])));for(a=0,i=new Array(f);a=u)){(t.data!==n||t.next)&&(0===l&&(p+=(l=Uc(e))*l),0===h&&(p+=(h=Uc(e))*h),p(t=(Lc*t+jc)%Hc)/Hc}();function l(){h(),f.call("tick",n),e1?(null==e?u.delete(t):u.set(t,p(e)),n):u.get(t)},find:function(n,e,r){var i,o,a,u,c,f=0,s=t.length;for(null==r?r=1/0:r*=r,f=0;f1?(f.on(t,e),n):f.on(t)}}},t.forceX=function(t){var n,e,r,i=qc(.1);function o(t){for(var i,o=0,a=n.length;o=.12&&i<.234&&r>=-.425&&r<-.214?u:i>=.166&&i<.234&&r>=-.214&&r<-.115?c:a).invert(t)},s.stream=function(e){return t&&n===e?t:(r=[a.stream(n=e),u.stream(e),c.stream(e)],i=r.length,t={point:function(t,n){for(var e=-1;++ejs(r[0],r[1])&&(r[1]=i[1]),js(i[0],r[1])>js(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(a=-1/0,n=0,r=o[e=o.length-1];n<=e;r=i,++n)i=o[n],(u=js(r[1],i[0]))>a&&(a=u,Wf=i[0],Kf=r[1])}return is=os=null,Wf===1/0||Zf===1/0?[[NaN,NaN],[NaN,NaN]]:[[Wf,Zf],[Kf,Qf]]},t.geoCentroid=function(t){ms=xs=ws=Ms=Ts=As=Ss=Es=0,Ns=new T,ks=new T,Cs=new T,Lf(t,Gs);var n=+Ns,e=+ks,r=+Cs,i=Ef(n,e,r);return i=0))throw new RangeError(`invalid digits: ${t}`);i=n}return null===n&&(r=new ed(i)),a},a.projection(t).digits(i).context(n)},t.geoProjection=yd,t.geoProjectionMutator=vd,t.geoRotation=ll,t.geoStereographic=function(){return yd(Bd).scale(250).clipAngle(142)},t.geoStereographicRaw=Bd,t.geoStream=Lf,t.geoTransform=function(t){return{stream:id(t)}},t.geoTransverseMercator=function(){var t=Ed(Yd),n=t.center,e=t.rotate;return t.center=function(t){return arguments.length?n([-t[1],t[0]]):[(t=n())[1],-t[0]]},t.rotate=function(t){return arguments.length?e([t[0],t[1],t.length>2?t[2]+90:90]):[(t=e())[0],t[1],t[2]-90]},e([0,0,90]).scale(159.155)},t.geoTransverseMercatorRaw=Yd,t.gray=function(t,n){return new ur(t,0,0,null==n?1:n)},t.greatest=ot,t.greatestIndex=function(t,e=n){if(1===e.length)return tt(t,e);let r,i=-1,o=-1;for(const n of t)++o,(i<0?0===e(n,n):e(n,r)>0)&&(r=n,i=o);return i},t.group=C,t.groupSort=function(t,e,r){return(2!==e.length?U($(t,e,r),(([t,e],[r,i])=>n(e,i)||n(t,r))):U(C(t,r),(([t,r],[i,o])=>e(r,o)||n(t,i)))).map((([t])=>t))},t.groups=P,t.hcl=dr,t.hierarchy=Gd,t.histogram=Q,t.hsl=He,t.html=Ec,t.image=function(t,n){return new Promise((function(e,r){var i=new Image;for(var o in n)i[o]=n[o];i.onerror=r,i.onload=function(){e(i)},i.src=t}))},t.index=function(t,...n){return F(t,k,R,n)},t.indexes=function(t,...n){return F(t,Array.from,R,n)},t.interpolate=Gr,t.interpolateArray=function(t,n){return(Ir(n)?Ur:Or)(t,n)},t.interpolateBasis=Er,t.interpolateBasisClosed=Nr,t.interpolateBlues=Gb,t.interpolateBrBG=ob,t.interpolateBuGn=Mb,t.interpolateBuPu=Ab,t.interpolateCividis=function(t){return t=Math.max(0,Math.min(1,t)),"rgb("+Math.max(0,Math.min(255,Math.round(-4.54-t*(35.34-t*(2381.73-t*(6402.7-t*(7024.72-2710.57*t)))))))+", "+Math.max(0,Math.min(255,Math.round(32.49+t*(170.73+t*(52.82-t*(131.46-t*(176.58-67.37*t)))))))+", "+Math.max(0,Math.min(255,Math.round(81.24+t*(442.36-t*(2482.43-t*(6167.24-t*(6614.94-2475.67*t)))))))+")"},t.interpolateCool=am,t.interpolateCubehelix=li,t.interpolateCubehelixDefault=im,t.interpolateCubehelixLong=hi,t.interpolateDate=Br,t.interpolateDiscrete=function(t){var n=t.length;return function(e){return t[Math.max(0,Math.min(n-1,Math.floor(e*n)))]}},t.interpolateGnBu=Eb,t.interpolateGreens=Wb,t.interpolateGreys=Kb,t.interpolateHcl=ci,t.interpolateHclLong=fi,t.interpolateHsl=oi,t.interpolateHslLong=ai,t.interpolateHue=function(t,n){var e=Pr(+t,+n);return function(t){var n=e(t);return n-360*Math.floor(n/360)}},t.interpolateInferno=pm,t.interpolateLab=function(t,n){var e=$r((t=ar(t)).l,(n=ar(n)).l),r=$r(t.a,n.a),i=$r(t.b,n.b),o=$r(t.opacity,n.opacity);return function(n){return t.l=e(n),t.a=r(n),t.b=i(n),t.opacity=o(n),t+""}},t.interpolateMagma=dm,t.interpolateNumber=Yr,t.interpolateNumberArray=Ur,t.interpolateObject=Lr,t.interpolateOrRd=kb,t.interpolateOranges=rm,t.interpolatePRGn=ub,t.interpolatePiYG=fb,t.interpolatePlasma=gm,t.interpolatePuBu=$b,t.interpolatePuBuGn=Pb,t.interpolatePuOr=lb,t.interpolatePuRd=Rb,t.interpolatePurples=Jb,t.interpolateRainbow=function(t){(t<0||t>1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return um.h=360*t-100,um.s=1.5-1.5*n,um.l=.8-.9*n,um+""},t.interpolateRdBu=db,t.interpolateRdGy=gb,t.interpolateRdPu=qb,t.interpolateRdYlBu=vb,t.interpolateRdYlGn=bb,t.interpolateReds=nm,t.interpolateRgb=Dr,t.interpolateRgbBasis=Fr,t.interpolateRgbBasisClosed=qr,t.interpolateRound=Vr,t.interpolateSinebow=function(t){var n;return t=(.5-t)*Math.PI,cm.r=255*(n=Math.sin(t))*n,cm.g=255*(n=Math.sin(t+fm))*n,cm.b=255*(n=Math.sin(t+sm))*n,cm+""},t.interpolateSpectral=xb,t.interpolateString=Xr,t.interpolateTransformCss=ti,t.interpolateTransformSvg=ni,t.interpolateTurbo=function(t){return t=Math.max(0,Math.min(1,t)),"rgb("+Math.max(0,Math.min(255,Math.round(34.61+t*(1172.33-t*(10793.56-t*(33300.12-t*(38394.49-14825.05*t)))))))+", "+Math.max(0,Math.min(255,Math.round(23.31+t*(557.33+t*(1225.33-t*(3574.96-t*(1073.77+707.56*t)))))))+", "+Math.max(0,Math.min(255,Math.round(27.2+t*(3211.1-t*(15327.97-t*(27814-t*(22569.18-6838.66*t)))))))+")"},t.interpolateViridis=hm,t.interpolateWarm=om,t.interpolateYlGn=Bb,t.interpolateYlGnBu=Ib,t.interpolateYlOrBr=Lb,t.interpolateYlOrRd=Hb,t.interpolateZoom=ri,t.interrupt=Gi,t.intersection=function(t,...n){t=new InternSet(t),n=n.map(vt);t:for(const e of t)for(const r of n)if(!r.has(e)){t.delete(e);continue t}return t},t.interval=function(t,n,e){var r=new Ei,i=n;return null==n?(r.restart(t,n,e),r):(r._restart=r.restart,r.restart=function(t,n,e){n=+n,e=null==e?Ai():+e,r._restart((function o(a){a+=i,r._restart(o,i+=n,e),t(a)}),n,e)},r.restart(t,n,e),r)},t.isoFormat=D_,t.isoParse=F_,t.json=function(t,n){return fetch(t,n).then(Tc)},t.lab=ar,t.lch=function(t,n,e,r){return 1===arguments.length?hr(t):new pr(e,n,t,null==r?1:r)},t.least=function(t,e=n){let r,i=!1;if(1===e.length){let o;for(const a of t){const t=e(a);(i?n(t,o)<0:0===n(t,t))&&(r=a,o=t,i=!0)}}else for(const n of t)(i?e(n,r)<0:0===e(n,n))&&(r=n,i=!0);return r},t.leastIndex=ht,t.line=Ym,t.lineRadial=Zm,t.link=ax,t.linkHorizontal=function(){return ax(nx)},t.linkRadial=function(){const t=ax(rx);return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t},t.linkVertical=function(){return ax(ex)},t.local=Qn,t.map=function(t,n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");if("function"!=typeof n)throw new TypeError("mapper is not a function");return Array.from(t,((e,r)=>n(e,r,t)))},t.matcher=Vt,t.max=J,t.maxIndex=tt,t.mean=function(t,n){let e=0,r=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(++e,r+=n);else{let i=-1;for(let o of t)null!=(o=n(o,++i,t))&&(o=+o)>=o&&(++e,r+=o)}if(e)return r/e},t.median=function(t,n){return at(t,.5,n)},t.medianIndex=function(t,n){return ct(t,.5,n)},t.merge=ft,t.min=nt,t.minIndex=et,t.mode=function(t,n){const e=new InternMap;if(void 0===n)for(let n of t)null!=n&&n>=n&&e.set(n,(e.get(n)||0)+1);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&i>=i&&e.set(i,(e.get(i)||0)+1)}let r,i=0;for(const[t,n]of e)n>i&&(i=n,r=t);return r},t.namespace=It,t.namespaces=Ut,t.nice=Z,t.now=Ai,t.pack=function(){var t=null,n=1,e=1,r=np;function i(i){const o=ap();return i.x=n/2,i.y=e/2,t?i.eachBefore(xp(t)).eachAfter(wp(r,.5,o)).eachBefore(Mp(1)):i.eachBefore(xp(mp)).eachAfter(wp(np,1,o)).eachAfter(wp(r,i.r/Math.min(n,e),o)).eachBefore(Mp(Math.min(n,e)/(2*i.r))),i}return i.radius=function(n){return arguments.length?(t=Jd(n),i):t},i.size=function(t){return arguments.length?(n=+t[0],e=+t[1],i):[n,e]},i.padding=function(t){return arguments.length?(r="function"==typeof t?t:ep(+t),i):r},i},t.packEnclose=function(t){return up(t,ap())},t.packSiblings=function(t){return bp(t,ap()),t},t.pairs=function(t,n=st){const e=[];let r,i=!1;for(const o of t)i&&e.push(n(r,o)),r=o,i=!0;return e},t.partition=function(){var t=1,n=1,e=0,r=!1;function i(i){var o=i.height+1;return i.x0=i.y0=e,i.x1=t,i.y1=n/o,i.eachBefore(function(t,n){return function(r){r.children&&Ap(r,r.x0,t*(r.depth+1)/n,r.x1,t*(r.depth+2)/n);var i=r.x0,o=r.y0,a=r.x1-e,u=r.y1-e;a0&&(d+=l);for(null!=n?p.sort((function(t,e){return n(g[t],g[e])})):null!=e&&p.sort((function(t,n){return e(a[t],a[n])})),u=0,f=d?(v-h*b)/d:0;u0?l*f:0)+b,g[c]={data:a[c],index:u,value:l,startAngle:y,endAngle:s,padAngle:_};return g}return a.value=function(n){return arguments.length?(t="function"==typeof n?n:ym(+n),a):t},a.sortValues=function(t){return arguments.length?(n=t,e=null,a):n},a.sort=function(t){return arguments.length?(e=t,n=null,a):e},a.startAngle=function(t){return arguments.length?(r="function"==typeof t?t:ym(+t),a):r},a.endAngle=function(t){return arguments.length?(i="function"==typeof t?t:ym(+t),a):i},a.padAngle=function(t){return arguments.length?(o="function"==typeof t?t:ym(+t),a):o},a},t.piecewise=di,t.pointRadial=Qm,t.pointer=ne,t.pointers=function(t,n){return t.target&&(t=te(t),void 0===n&&(n=t.currentTarget),t=t.touches||[t]),Array.from(t,(t=>ne(t,n)))},t.polygonArea=function(t){for(var n,e=-1,r=t.length,i=t[r-1],o=0;++eu!=f>u&&a<(c-e)*(u-r)/(f-r)+e&&(s=!s),c=e,f=r;return s},t.polygonHull=function(t){if((e=t.length)<3)return null;var n,e,r=new Array(e),i=new Array(e);for(n=0;n=0;--n)f.push(t[r[o[n]][2]]);for(n=+u;n(n=1664525*n+1013904223|0,lg*(n>>>0))},t.randomLogNormal=Kp,t.randomLogistic=fg,t.randomNormal=Zp,t.randomPareto=ng,t.randomPoisson=sg,t.randomUniform=Vp,t.randomWeibull=ug,t.range=lt,t.rank=function(t,e=n){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");let r=Array.from(t);const i=new Float64Array(r.length);2!==e.length&&(r=r.map(e),e=n);const o=(t,n)=>e(r[t],r[n]);let a,u;return(t=Uint32Array.from(r,((t,n)=>n))).sort(e===n?(t,n)=>O(r[t],r[n]):I(o)),t.forEach(((t,n)=>{const e=o(t,void 0===a?t:a);e>=0?((void 0===a||e>0)&&(a=t,u=n),i[t]=u):i[t]=NaN})),i},t.reduce=function(t,n,e){if("function"!=typeof n)throw new TypeError("reducer is not a function");const r=t[Symbol.iterator]();let i,o,a=-1;if(arguments.length<3){if(({done:i,value:e}=r.next()),i)return;++a}for(;({done:i,value:o}=r.next()),!i;)e=n(e,o,++a,t);return e},t.reverse=function(t){if("function"!=typeof t[Symbol.iterator])throw new TypeError("values is not iterable");return Array.from(t).reverse()},t.rgb=Fe,t.ribbon=function(){return Wa()},t.ribbonArrow=function(){return Wa(Va)},t.rollup=$,t.rollups=D,t.scaleBand=yg,t.scaleDiverging=function t(){var n=Ng(L_()(mg));return n.copy=function(){return B_(n,t())},dg.apply(n,arguments)},t.scaleDivergingLog=function t(){var n=Fg(L_()).domain([.1,1,10]);return n.copy=function(){return B_(n,t()).base(n.base())},dg.apply(n,arguments)},t.scaleDivergingPow=j_,t.scaleDivergingSqrt=function(){return j_.apply(null,arguments).exponent(.5)},t.scaleDivergingSymlog=function t(){var n=Ig(L_());return n.copy=function(){return B_(n,t()).constant(n.constant())},dg.apply(n,arguments)},t.scaleIdentity=function t(n){var e;function r(t){return null==t||isNaN(t=+t)?e:t}return r.invert=r,r.domain=r.range=function(t){return arguments.length?(n=Array.from(t,_g),r):n.slice()},r.unknown=function(t){return arguments.length?(e=t,r):e},r.copy=function(){return t(n).unknown(e)},n=arguments.length?Array.from(n,_g):[0,1],Ng(r)},t.scaleImplicit=pg,t.scaleLinear=function t(){var n=Sg();return n.copy=function(){return Tg(n,t())},hg.apply(n,arguments),Ng(n)},t.scaleLog=function t(){const n=Fg(Ag()).domain([1,10]);return n.copy=()=>Tg(n,t()).base(n.base()),hg.apply(n,arguments),n},t.scaleOrdinal=gg,t.scalePoint=function(){return vg(yg.apply(null,arguments).paddingInner(1))},t.scalePow=jg,t.scaleQuantile=function t(){var e,r=[],i=[],o=[];function a(){var t=0,n=Math.max(1,i.length);for(o=new Array(n-1);++t0?o[n-1]:r[0],n=i?[o[i-1],r]:[o[n-1],o[n]]},u.unknown=function(t){return arguments.length?(n=t,u):u},u.thresholds=function(){return o.slice()},u.copy=function(){return t().domain([e,r]).range(a).unknown(n)},hg.apply(Ng(u),arguments)},t.scaleRadial=function t(){var n,e=Sg(),r=[0,1],i=!1;function o(t){var r=function(t){return Math.sign(t)*Math.sqrt(Math.abs(t))}(e(t));return isNaN(r)?n:i?Math.round(r):r}return o.invert=function(t){return e.invert(Hg(t))},o.domain=function(t){return arguments.length?(e.domain(t),o):e.domain()},o.range=function(t){return arguments.length?(e.range((r=Array.from(t,_g)).map(Hg)),o):r.slice()},o.rangeRound=function(t){return o.range(t).round(!0)},o.round=function(t){return arguments.length?(i=!!t,o):i},o.clamp=function(t){return arguments.length?(e.clamp(t),o):e.clamp()},o.unknown=function(t){return arguments.length?(n=t,o):n},o.copy=function(){return t(e.domain(),r).round(i).clamp(e.clamp()).unknown(n)},hg.apply(o,arguments),Ng(o)},t.scaleSequential=function t(){var n=Ng(O_()(mg));return n.copy=function(){return B_(n,t())},dg.apply(n,arguments)},t.scaleSequentialLog=function t(){var n=Fg(O_()).domain([1,10]);return n.copy=function(){return B_(n,t()).base(n.base())},dg.apply(n,arguments)},t.scaleSequentialPow=Y_,t.scaleSequentialQuantile=function t(){var e=[],r=mg;function i(t){if(null!=t&&!isNaN(t=+t))return r((s(e,t,1)-1)/(e.length-1))}return i.domain=function(t){if(!arguments.length)return e.slice();e=[];for(let n of t)null==n||isNaN(n=+n)||e.push(n);return e.sort(n),i},i.interpolator=function(t){return arguments.length?(r=t,i):r},i.range=function(){return e.map(((t,n)=>r(n/(e.length-1))))},i.quantiles=function(t){return Array.from({length:t+1},((n,r)=>at(e,r/t)))},i.copy=function(){return t(r).domain(e)},dg.apply(i,arguments)},t.scaleSequentialSqrt=function(){return Y_.apply(null,arguments).exponent(.5)},t.scaleSequentialSymlog=function t(){var n=Ig(O_());return n.copy=function(){return B_(n,t()).constant(n.constant())},dg.apply(n,arguments)},t.scaleSqrt=function(){return jg.apply(null,arguments).exponent(.5)},t.scaleSymlog=function t(){var n=Ig(Ag());return n.copy=function(){return Tg(n,t()).constant(n.constant())},hg.apply(n,arguments)},t.scaleThreshold=function t(){var n,e=[.5],r=[0,1],i=1;function o(t){return null!=t&&t<=t?r[s(e,t,0,i)]:n}return o.domain=function(t){return arguments.length?(e=Array.from(t),i=Math.min(e.length,r.length-1),o):e.slice()},o.range=function(t){return arguments.length?(r=Array.from(t),i=Math.min(e.length,r.length-1),o):r.slice()},o.invertExtent=function(t){var n=r.indexOf(t);return[e[n-1],e[n]]},o.unknown=function(t){return arguments.length?(n=t,o):n},o.copy=function(){return t().domain(e).range(r).unknown(n)},hg.apply(o,arguments)},t.scaleTime=function(){return hg.apply(I_(uv,cv,tv,Zy,xy,py,sy,ay,iy,t.timeFormat).domain([new Date(2e3,0,1),new Date(2e3,0,2)]),arguments)},t.scaleUtc=function(){return hg.apply(I_(ov,av,ev,Qy,Fy,yy,hy,cy,iy,t.utcFormat).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)]),arguments)},t.scan=function(t,n){const e=ht(t,n);return e<0?void 0:e},t.schemeAccent=G_,t.schemeBlues=Xb,t.schemeBrBG=ib,t.schemeBuGn=wb,t.schemeBuPu=Tb,t.schemeCategory10=X_,t.schemeDark2=V_,t.schemeGnBu=Sb,t.schemeGreens=Vb,t.schemeGreys=Zb,t.schemeObservable10=W_,t.schemeOrRd=Nb,t.schemeOranges=em,t.schemePRGn=ab,t.schemePaired=Z_,t.schemePastel1=K_,t.schemePastel2=Q_,t.schemePiYG=cb,t.schemePuBu=zb,t.schemePuBuGn=Cb,t.schemePuOr=sb,t.schemePuRd=Db,t.schemePurples=Qb,t.schemeRdBu=hb,t.schemeRdGy=pb,t.schemeRdPu=Fb,t.schemeRdYlBu=yb,t.schemeRdYlGn=_b,t.schemeReds=tm,t.schemeSet1=J_,t.schemeSet2=tb,t.schemeSet3=nb,t.schemeSpectral=mb,t.schemeTableau10=eb,t.schemeYlGn=Ob,t.schemeYlGnBu=Ub,t.schemeYlOrBr=Yb,t.schemeYlOrRd=jb,t.select=Zn,t.selectAll=function(t){return"string"==typeof t?new Vn([document.querySelectorAll(t)],[document.documentElement]):new Vn([Ht(t)],Gn)},t.selection=Wn,t.selector=jt,t.selectorAll=Gt,t.shuffle=dt,t.shuffler=pt,t.some=function(t,n){if("function"!=typeof n)throw new TypeError("test is not a function");let e=-1;for(const r of t)if(n(r,++e,t))return!0;return!1},t.sort=U,t.stack=function(){var t=ym([]),n=dw,e=hw,r=pw;function i(i){var o,a,u=Array.from(t.apply(this,arguments),gw),c=u.length,f=-1;for(const t of i)for(o=0,++f;o0)for(var e,r,i,o,a,u,c=0,f=t[n[0]].length;c0?(r[0]=o,r[1]=o+=i):i<0?(r[1]=a,r[0]=a+=i):(r[0]=0,r[1]=i)},t.stackOffsetExpand=function(t,n){if((r=t.length)>0){for(var e,r,i,o=0,a=t[0].length;o0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,a=1;afunction(t){t=`${t}`;let n=t.length;zp(t,n-1)&&!zp(t,n-2)&&(t=t.slice(0,-1));return"/"===t[0]?t:`/${t}`}(t(n,e,r)))),e=n.map(Pp),i=new Set(n).add("");for(const t of e)i.has(t)||(i.add(t),n.push(t),e.push(Pp(t)),h.push(Np));d=(t,e)=>n[e],p=(t,n)=>e[n]}for(a=0,i=h.length;a=0&&(f=h[t]).data===Np;--t)f.data=null}if(u.parent=Sp,u.eachBefore((function(t){t.depth=t.parent.depth+1,--i})).eachBefore(Kd),u.parent=null,i>0)throw new Error("cycle");return u}return r.id=function(t){return arguments.length?(n=Jd(t),r):n},r.parentId=function(t){return arguments.length?(e=Jd(t),r):e},r.path=function(n){return arguments.length?(t=Jd(n),r):t},r},t.style=_n,t.subset=function(t,n){return _t(n,t)},t.sum=function(t,n){let e=0;if(void 0===n)for(let n of t)(n=+n)&&(e+=n);else{let r=-1;for(let i of t)(i=+n(i,++r,t))&&(e+=i)}return e},t.superset=_t,t.svg=Nc,t.symbol=function(t,n){let e=null,r=km(i);function i(){let i;if(e||(e=i=r()),t.apply(this,arguments).draw(e,+n.apply(this,arguments)),i)return e=null,i+""||null}return t="function"==typeof t?t:ym(t||fx),n="function"==typeof n?n:ym(void 0===n?64:+n),i.type=function(n){return arguments.length?(t="function"==typeof n?n:ym(n),i):t},i.size=function(t){return arguments.length?(n="function"==typeof t?t:ym(+t),i):n},i.context=function(t){return arguments.length?(e=null==t?null:t,i):e},i},t.symbolAsterisk=cx,t.symbolCircle=fx,t.symbolCross=sx,t.symbolDiamond=dx,t.symbolDiamond2=px,t.symbolPlus=gx,t.symbolSquare=yx,t.symbolSquare2=vx,t.symbolStar=xx,t.symbolTimes=Px,t.symbolTriangle=Mx,t.symbolTriangle2=Ax,t.symbolWye=Cx,t.symbolX=Px,t.symbols=zx,t.symbolsFill=zx,t.symbolsStroke=$x,t.text=mc,t.thresholdFreedmanDiaconis=function(t,n,e){const r=v(t),i=at(t,.75)-at(t,.25);return r&&i?Math.ceil((e-n)/(2*i*Math.pow(r,-1/3))):1},t.thresholdScott=function(t,n,e){const r=v(t),i=w(t);return r&&i?Math.ceil((e-n)*Math.cbrt(r)/(3.49*i)):1},t.thresholdSturges=K,t.tickFormat=Eg,t.tickIncrement=V,t.tickStep=W,t.ticks=G,t.timeDay=py,t.timeDays=gy,t.timeFormatDefaultLocale=P_,t.timeFormatLocale=hv,t.timeFriday=Sy,t.timeFridays=$y,t.timeHour=sy,t.timeHours=ly,t.timeInterval=Vg,t.timeMillisecond=Wg,t.timeMilliseconds=Zg,t.timeMinute=ay,t.timeMinutes=uy,t.timeMonday=wy,t.timeMondays=ky,t.timeMonth=Zy,t.timeMonths=Ky,t.timeSaturday=Ey,t.timeSaturdays=Dy,t.timeSecond=iy,t.timeSeconds=oy,t.timeSunday=xy,t.timeSundays=Ny,t.timeThursday=Ay,t.timeThursdays=zy,t.timeTickInterval=cv,t.timeTicks=uv,t.timeTuesday=My,t.timeTuesdays=Cy,t.timeWednesday=Ty,t.timeWednesdays=Py,t.timeWeek=xy,t.timeWeeks=Ny,t.timeYear=tv,t.timeYears=nv,t.timeout=$i,t.timer=Ni,t.timerFlush=ki,t.transition=go,t.transpose=gt,t.tree=function(){var t=$p,n=1,e=1,r=null;function i(i){var c=function(t){for(var n,e,r,i,o,a=new Up(t,0),u=[a];n=u.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)u.push(e=n.children[i]=new Up(r[i],i)),e.parent=n;return(a.parent=new Up(null,0)).children=[a],a}(i);if(c.eachAfter(o),c.parent.m=-c.z,c.eachBefore(a),r)i.eachBefore(u);else{var f=i,s=i,l=i;i.eachBefore((function(t){t.xs.x&&(s=t),t.depth>l.depth&&(l=t)}));var h=f===s?1:t(f,s)/2,d=h-f.x,p=n/(s.x+h+d),g=e/(l.depth||1);i.eachBefore((function(t){t.x=(t.x+d)*p,t.y=t.depth*g}))}return i}function o(n){var e=n.children,r=n.parent.children,i=n.i?r[n.i-1]:null;if(e){!function(t){for(var n,e=0,r=0,i=t.children,o=i.length;--o>=0;)(n=i[o]).z+=e,n.m+=e,e+=n.s+(r+=n.c)}(n);var o=(e[0].z+e[e.length-1].z)/2;i?(n.z=i.z+t(n._,i._),n.m=n.z-o):n.z=o}else i&&(n.z=i.z+t(n._,i._));n.parent.A=function(n,e,r){if(e){for(var i,o=n,a=n,u=e,c=o.parent.children[0],f=o.m,s=a.m,l=u.m,h=c.m;u=Rp(u),o=Dp(o),u&&o;)c=Dp(c),(a=Rp(a)).a=n,(i=u.z+l-o.z-f+t(u._,o._))>0&&(Fp(qp(u,n,r),n,i),f+=i,s+=i),l+=u.m,f+=o.m,h+=c.m,s+=a.m;u&&!Rp(a)&&(a.t=u,a.m+=l-s),o&&!Dp(c)&&(c.t=o,c.m+=f-h,r=n)}return r}(n,i,n.parent.A||r[0])}function a(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function u(t){t.x*=n,t.y=t.depth*e}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.treemap=function(){var t=Yp,n=!1,e=1,r=1,i=[0],o=np,a=np,u=np,c=np,f=np;function s(t){return t.x0=t.y0=0,t.x1=e,t.y1=r,t.eachBefore(l),i=[0],n&&t.eachBefore(Tp),t}function l(n){var e=i[n.depth],r=n.x0+e,s=n.y0+e,l=n.x1-e,h=n.y1-e;l=e-1){var s=u[n];return s.x0=i,s.y0=o,s.x1=a,void(s.y1=c)}var l=f[n],h=r/2+l,d=n+1,p=e-1;for(;d>>1;f[g]c-o){var _=r?(i*v+a*y)/r:a;t(n,d,y,i,o,_,c),t(d,e,v,_,o,a,c)}else{var b=r?(o*v+c*y)/r:c;t(n,d,y,i,o,a,b),t(d,e,v,i,b,a,c)}}(0,c,t.value,n,e,r,i)},t.treemapDice=Ap,t.treemapResquarify=Lp,t.treemapSlice=Ip,t.treemapSliceDice=function(t,n,e,r,i){(1&t.depth?Ip:Ap)(t,n,e,r,i)},t.treemapSquarify=Yp,t.tsv=Mc,t.tsvFormat=lc,t.tsvFormatBody=hc,t.tsvFormatRow=pc,t.tsvFormatRows=dc,t.tsvFormatValue=gc,t.tsvParse=fc,t.tsvParseRows=sc,t.union=function(...t){const n=new InternSet;for(const e of t)for(const t of e)n.add(t);return n},t.unixDay=_y,t.unixDays=by,t.utcDay=yy,t.utcDays=vy,t.utcFriday=By,t.utcFridays=Vy,t.utcHour=hy,t.utcHours=dy,t.utcMillisecond=Wg,t.utcMilliseconds=Zg,t.utcMinute=cy,t.utcMinutes=fy,t.utcMonday=qy,t.utcMondays=jy,t.utcMonth=Qy,t.utcMonths=Jy,t.utcSaturday=Yy,t.utcSaturdays=Wy,t.utcSecond=iy,t.utcSeconds=oy,t.utcSunday=Fy,t.utcSundays=Ly,t.utcThursday=Oy,t.utcThursdays=Gy,t.utcTickInterval=av,t.utcTicks=ov,t.utcTuesday=Uy,t.utcTuesdays=Hy,t.utcWednesday=Iy,t.utcWednesdays=Xy,t.utcWeek=Fy,t.utcWeeks=Ly,t.utcYear=ev,t.utcYears=rv,t.variance=x,t.version="7.9.0",t.window=pn,t.xml=Sc,t.zip=function(){return gt(arguments)},t.zoom=function(){var t,n,e,r=Ew,i=Nw,o=zw,a=Cw,u=Pw,c=[0,1/0],f=[[-1/0,-1/0],[1/0,1/0]],s=250,l=ri,h=$t("start","zoom","end"),d=500,p=150,g=0,y=10;function v(t){t.property("__zoom",kw).on("wheel.zoom",T,{passive:!1}).on("mousedown.zoom",A).on("dblclick.zoom",S).filter(u).on("touchstart.zoom",E).on("touchmove.zoom",N).on("touchend.zoom touchcancel.zoom",k).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function _(t,n){return(n=Math.max(c[0],Math.min(c[1],n)))===t.k?t:new ww(n,t.x,t.y)}function b(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new ww(t.k,r,i)}function m(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function x(t,n,e,r){t.on("start.zoom",(function(){w(this,arguments).event(r).start()})).on("interrupt.zoom end.zoom",(function(){w(this,arguments).event(r).end()})).tween("zoom",(function(){var t=this,o=arguments,a=w(t,o).event(r),u=i.apply(t,o),c=null==e?m(u):"function"==typeof e?e.apply(t,o):e,f=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),s=t.__zoom,h="function"==typeof n?n.apply(t,o):n,d=l(s.invert(c).concat(f/s.k),h.invert(c).concat(f/h.k));return function(t){if(1===t)t=h;else{var n=d(t),e=f/n[2];t=new ww(e,c[0]-n[0]*e,c[1]-n[1]*e)}a.zoom(null,t)}}))}function w(t,n,e){return!e&&t.__zooming||new M(t,n)}function M(t,n){this.that=t,this.args=n,this.active=0,this.sourceEvent=null,this.extent=i.apply(t,n),this.taps=0}function T(t,...n){if(r.apply(this,arguments)){var e=w(this,n).event(t),i=this.__zoom,u=Math.max(c[0],Math.min(c[1],i.k*Math.pow(2,a.apply(this,arguments)))),s=ne(t);if(e.wheel)e.mouse[0][0]===s[0]&&e.mouse[0][1]===s[1]||(e.mouse[1]=i.invert(e.mouse[0]=s)),clearTimeout(e.wheel);else{if(i.k===u)return;e.mouse=[s,i.invert(s)],Gi(this),e.start()}Sw(t),e.wheel=setTimeout((function(){e.wheel=null,e.end()}),p),e.zoom("mouse",o(b(_(i,u),e.mouse[0],e.mouse[1]),e.extent,f))}}function A(t,...n){if(!e&&r.apply(this,arguments)){var i=t.currentTarget,a=w(this,n,!0).event(t),u=Zn(t.view).on("mousemove.zoom",(function(t){if(Sw(t),!a.moved){var n=t.clientX-s,e=t.clientY-l;a.moved=n*n+e*e>g}a.event(t).zoom("mouse",o(b(a.that.__zoom,a.mouse[0]=ne(t,i),a.mouse[1]),a.extent,f))}),!0).on("mouseup.zoom",(function(t){u.on("mousemove.zoom mouseup.zoom",null),ue(t.view,a.moved),Sw(t),a.event(t).end()}),!0),c=ne(t,i),s=t.clientX,l=t.clientY;ae(t.view),Aw(t),a.mouse=[c,this.__zoom.invert(c)],Gi(this),a.start()}}function S(t,...n){if(r.apply(this,arguments)){var e=this.__zoom,a=ne(t.changedTouches?t.changedTouches[0]:t,this),u=e.invert(a),c=e.k*(t.shiftKey?.5:2),l=o(b(_(e,c),a,u),i.apply(this,n),f);Sw(t),s>0?Zn(this).transition().duration(s).call(x,l,a,t):Zn(this).call(v.transform,l,a,t)}}function E(e,...i){if(r.apply(this,arguments)){var o,a,u,c,f=e.touches,s=f.length,l=w(this,i,e.changedTouches.length===s).event(e);for(Aw(e),a=0;a int | None: + try: + return int(value) + except (TypeError, ValueError): + return None + + +def merge_line_ranges(ranges: Sequence[tuple[int, int]]) -> list[tuple[int, int]]: + ordered = sorted((start, end) for start, end in ranges if start > 0 and end >= start) + merged: list[tuple[int, int]] = [] + for start, end in ordered: + if not merged or start > merged[-1][1] + 1: + merged.append((start, end)) + else: + merged[-1] = (merged[-1][0], max(merged[-1][1], end)) + return merged + + +def changed_line_ranges_from_diff(diff: Any) -> list[tuple[int, int]]: + if not isinstance(diff, str) or "@@" not in diff: + return [] + ranges: list[tuple[int, int]] = [] + new_line: int | None = None + for raw_line in diff.splitlines(): + match = _HUNK_RE.search(raw_line) + if match: + new_line = int(match.group(1)) + continue + if new_line is None: + continue + if raw_line.startswith("\\") or raw_line.startswith("+++") or raw_line.startswith("---"): + continue + prefix = raw_line[:1] + if prefix == "+": + ranges.append((new_line, new_line)) + new_line += 1 + elif prefix == "-": + continue + else: + new_line += 1 + return merge_line_ranges(ranges) + + +def changed_line_ranges_by_file(code_deltas: Sequence[Mapping[str, Any]]) -> dict[str, list[tuple[int, int]]]: + ranges_by_file: dict[str, list[tuple[int, int]]] = {} + for delta in code_deltas: + file_path = str(delta.get("file") or delta.get("path") or delta.get("after") or delta.get("before") or "") + if not file_path: + continue + ranges = changed_line_ranges_from_diff(delta.get("diff")) + if file_path in ranges_by_file: + ranges_by_file[file_path] = merge_line_ranges(ranges_by_file[file_path] + ranges) + else: + ranges_by_file[file_path] = ranges + return ranges_by_file + + +def line_range_from_mapping(row: Mapping[str, Any]) -> tuple[int, int] | None: + nested = row.get("line_range") if isinstance(row.get("line_range"), Mapping) else {} + start = _to_int(nested.get("start") if nested else None) + end = _to_int(nested.get("end") if nested else None) + if start is None: + start = _to_int( + row.get("line_start") + or row.get("start_line") + or row.get("lineno") + or row.get("line") + or row.get("start") + ) + if end is None: + end = _to_int(row.get("line_end") or row.get("end_line") or row.get("end")) + if start is None: + return None + return (start, end if end is not None else start) + + +def line_range_overlaps_any(line_range: tuple[int, int] | None, ranges: Sequence[tuple[int, int]]) -> bool: + if line_range is None or not ranges: + return False + start, end = line_range + return any(start <= range_end and end >= range_start for range_start, range_end in ranges) + + +def row_overlaps_changed_lines( + row: Mapping[str, Any], + file_path: str, + ranges_by_file: Mapping[str, Sequence[tuple[int, int]]], +) -> bool: + return line_range_overlaps_any(line_range_from_mapping(row), ranges_by_file.get(file_path, [])) + + +def is_file_level_node(node_id: Any, row: Mapping[str, Any], file_path: str) -> bool: + node_id_text = str(node_id or "") + node_type = str(row.get("type") or row.get("kind") or "").lower() + if node_type in {"file", "module"}: + return True + return node_id_text == file_path or (":" not in node_id_text and str(row.get("path") or row.get("file") or row.get("module") or "") == file_path) diff --git a/CoderMind/scripts/common/git_utils.py b/CoderMind/scripts/common/git_utils.py index 3c72866..b08e45c 100644 --- a/CoderMind/scripts/common/git_utils.py +++ b/CoderMind/scripts/common/git_utils.py @@ -85,12 +85,12 @@ class GitRunner: def __init__( self, repo_path: str, - main_branch: str = "main", + main_branch: str = MAIN_BRANCH, logger: Optional[logging.Logger] = None ): self.repo_path = Path(repo_path) self.logger = logger or logging.getLogger(__name__) - self.main_branch = self.MAIN_BRANCH + self.main_branch = main_branch # Ensure repo exists and is a git repo self._ensure_git_repository() @@ -149,7 +149,7 @@ def _ensure_git_repository(self) -> None: if not git_dir.exists(): self.logger.info("Initializing git repository...") self.repo_path.mkdir(parents=True, exist_ok=True) - self.run_git(["init", "-b", self.MAIN_BRANCH]) + self.run_git(["init", "-b", self.main_branch]) # Configure safe directory self.run_git([ @@ -391,11 +391,11 @@ def get_diff( to_commit: str = "HEAD" ) -> str: """Get diff between commits. - + Args: from_commit: Start commit (default: parent of to_commit) to_commit: End commit (default: HEAD) - + Returns: Diff content as string """ @@ -403,9 +403,24 @@ def get_diff( result = self.run_git(["diff", from_commit, to_commit]) else: result = self.run_git(["diff", f"{to_commit}^", to_commit]) - + return result.stdout if result.success else "" - + + def get_file_diffs( + self, + from_commit: Optional[str] = None, + to_commit: str = "HEAD", + files: Optional[List[str]] = None, + ) -> List[Dict[str, str]]: + """Get per-file diff rows between commits.""" + return file_diffs_between( + self.repo_path, + from_commit, + to_commit, + files=files, + py_only=False, + ) + def get_changed_files( self, from_commit: Optional[str] = None, @@ -565,6 +580,34 @@ def read_head(repo_dir: str | Path) -> Optional[dict]: } +def git_workspace_prefix(workspace_dir: str | Path) -> str: + """Return the path from the git root to ``workspace_dir``. + + Returns ``""`` when ``workspace_dir`` is the git root or when git metadata + cannot be read. This keeps callers safe outside git repositories. + """ + if not workspace_dir: + return "" + workspace_path = Path(workspace_dir) + if not workspace_path.is_dir(): + return "" + + git_root = _run_git_readonly( + ["rev-parse", "--show-toplevel"], + workspace_path, + ) + if not git_root: + return "" + + try: + rel = workspace_path.resolve().relative_to(Path(git_root).resolve()) + except ValueError: + return "" + + rel_str = rel.as_posix() + return "" if rel_str == "." else rel_str + + # --------------------------------------------------------------------------- # Diff helpers — produce ``(modified, renames)`` from various git scopes. # --------------------------------------------------------------------------- @@ -589,6 +632,52 @@ def read_head(repo_dir: str | Path) -> Optional[dict]: _GIT_STATUS_COPY_PREFIX = "C" +def _parse_name_status_rows( + raw: Optional[str], + *, + py_only: bool = True, +) -> List[Dict[str, str]]: + rows: List[Dict[str, str]] = [] + if not raw: + return rows + + def _keep(p: str) -> bool: + return (not py_only) or p.endswith(".py") + + for line in raw.splitlines(): + parts = line.split("\t") + if len(parts) < 2: + continue + status = parts[0] + if status.startswith(_GIT_STATUS_RENAME_PREFIX) or status.startswith( + _GIT_STATUS_COPY_PREFIX + ): + if len(parts) < 3: + continue + old_path, new_path = parts[1], parts[2] + if _keep(new_path) or _keep(old_path): + rows.append({ + "file": new_path, + "change_type": "rename" if status.startswith(_GIT_STATUS_RENAME_PREFIX) else "copy", + "old_file": old_path, + "status": status, + }) + continue + path = parts[1] + if not _keep(path): + continue + if status == _GIT_STATUS_ADDED: + change_type = "add" + elif status == _GIT_STATUS_DELETED: + change_type = "delete" + elif status == _GIT_STATUS_MODIFIED: + change_type = "modify" + else: + continue + rows.append({"file": path, "change_type": change_type, "status": status}) + return rows + + def _parse_name_status( raw: Optional[str], *, @@ -616,40 +705,76 @@ def _parse_name_status( if not raw: return modified, renames - def _keep(p: str) -> bool: - return (not py_only) or p.endswith(".py") - - for line in raw.splitlines(): - parts = line.split("\t") - if len(parts) < 2: - continue - status = parts[0] - if status.startswith(_GIT_STATUS_RENAME_PREFIX) or status.startswith( - _GIT_STATUS_COPY_PREFIX - ): - if len(parts) < 3: + for row in _parse_name_status_rows(raw, py_only=py_only): + path = row.get("file", "") + if row.get("change_type") in ("rename", "copy"): + old_path = row.get("old_file", "") + if old_path and path: + renames[old_path] = path + if py_only and not path.endswith(".py"): continue - old_path, new_path = parts[1], parts[2] - if _keep(new_path) or _keep(old_path): - renames[old_path] = new_path - # update_files() treats the OLD path as a deletion (via - # ``renames``) and the NEW path as something it must - # reparse — so we surface the new path through the - # modified list as well. - if _keep(new_path): - modified.append(new_path) - continue - path = parts[1] - if not _keep(path): - continue - if status in (_GIT_STATUS_ADDED, _GIT_STATUS_DELETED, _GIT_STATUS_MODIFIED): + if path: modified.append(path) - # Type / unmerged / other status letters → ignore (caller will - # fall back to full sync via the safety threshold if there are - # many of them). return modified, renames +def _diff_range_args( + from_commit: Optional[str] = None, + to_commit: str = "HEAD", +) -> List[str]: + if from_commit: + return [from_commit, to_commit] + return [f"{to_commit}^", to_commit] + + +def file_diffs_between( + repo_dir: str | Path, + from_commit: Optional[str] = None, + to_commit: str = "HEAD", + *, + files: Optional[List[str]] = None, + py_only: bool = False, +) -> List[Dict[str, str]]: + """Return per-file diff rows without mutating git state.""" + if not repo_dir: + return [] + repo_path = Path(repo_dir) + if not repo_path.is_dir(): + return [] + + range_args = _diff_range_args(from_commit, to_commit) + raw_status = _run_git_readonly( + ["diff", "--relative", "--name-status", "-M", *range_args], + repo_path, + ) + rows = _parse_name_status_rows(raw_status, py_only=py_only) + if not rows: + return [] + + selected = {str(path) for path in files or [] if path} + diff_rows: List[Dict[str, str]] = [] + for row in rows: + file_path = row.get("file", "") + old_file = row.get("old_file", "") + if selected and file_path not in selected and old_file not in selected: + continue + pathspecs = [path for path in [old_file, file_path] if path] + raw_diff = _run_git_readonly( + ["diff", *range_args, "--", *pathspecs], + repo_path, + timeout=10.0, + ) + diff_row = { + "file": file_path, + "change_type": row.get("change_type", "modify"), + "diff": raw_diff or "", + } + if old_file: + diff_row["old_file"] = old_file + diff_rows.append(diff_row) + return diff_rows + + def staged_changes( repo_dir: str | Path, ) -> Tuple[List[str], Dict[str, str]]: @@ -669,14 +794,14 @@ def staged_changes( if not repo_path.is_dir(): return [], {} raw = _run_git_readonly( - ["diff", "--cached", "--name-status", "-M", "HEAD"], + ["diff", "--cached", "--relative", "--name-status", "-M", "HEAD"], repo_path, ) if raw is None: # ``HEAD`` may not exist yet (unborn branch); try without it so # the very first commit's staged files still get picked up. raw = _run_git_readonly( - ["diff", "--cached", "--name-status", "-M"], + ["diff", "--cached", "--relative", "--name-status", "-M"], repo_path, ) return _parse_name_status(raw) @@ -706,7 +831,7 @@ def working_tree_changes( return [], {} raw = _run_git_readonly( - ["diff", "--name-status", "-M", "HEAD"], + ["diff", "--relative", "--name-status", "-M", "HEAD"], repo_path, ) modified, renames = _parse_name_status(raw) @@ -749,7 +874,7 @@ def changed_files_between( if not repo_path.is_dir(): return [], {} raw = _run_git_readonly( - ["diff", "--name-status", "-M", f"{old_ref}..{new_ref}"], + ["diff", "--relative", "--name-status", "-M", f"{old_ref}..{new_ref}"], repo_path, ) return _parse_name_status(raw) diff --git a/CoderMind/scripts/common/llm_client.py b/CoderMind/scripts/common/llm_client.py index e9fddd7..4628e48 100644 --- a/CoderMind/scripts/common/llm_client.py +++ b/CoderMind/scripts/common/llm_client.py @@ -419,8 +419,8 @@ def generate( self.logger.warning(error) except subprocess.TimeoutExpired: - error = f"LLM call timed out after {timeout}s" - self.logger.warning(f"LLM call timed out (attempt {attempt + 1})") + error = f"AI CLI subprocess timed out after {timeout}s" + self.logger.warning(f"AI CLI subprocess timed out (attempt {attempt + 1})") except Exception as e: error = str(e) self.logger.warning(f"LLM call error: {e}") @@ -465,8 +465,8 @@ def generate( self.logger.warning(f"Failed to record LLM interaction completion: {e}") if not response: - raise RuntimeError(f"Failed to get response from LLM after {max_retries} retries: {error}") - + raise RuntimeError(f"AI CLI did not return a response after {max_retries} attempts: {error}") + return response def generate_with_record( diff --git a/CoderMind/scripts/common/paths.py b/CoderMind/scripts/common/paths.py index bebd0d0..15c62b0 100644 --- a/CoderMind/scripts/common/paths.py +++ b/CoderMind/scripts/common/paths.py @@ -301,7 +301,10 @@ def cmd_for(script_relpath: str) -> str: RPG_EDIT_PLAN_FILE = DATA_DIR / "rpg_edit_plan.json" RPG_EDIT_IMPACT_FILE = DATA_DIR / "rpg_edit_impact.json" +RPG_EDIT_VALIDATE_FILE = DATA_DIR / "rpg_edit_validate.json" +RPG_EDIT_LOCATE_FILE = DATA_DIR / "rpg_edit_locate.json" RPG_EDIT_CODE_RESULT_FILE = DATA_DIR / "rpg_edit_code_result.json" +RPG_EDIT_APPLY_RESULT_FILE = DATA_DIR / "rpg_edit_apply_result.json" RPG_EDIT_REVIEW_RESULT_FILE = DATA_DIR / "rpg_edit_review_result.json" diff --git a/CoderMind/scripts/common/run_events.py b/CoderMind/scripts/common/run_events.py new file mode 100644 index 0000000..99ccf11 --- /dev/null +++ b/CoderMind/scripts/common/run_events.py @@ -0,0 +1,238 @@ +"""Structured event contract for command run reports.""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Mapping, Sequence + + +def _to_plain(value: Any) -> Any: + if hasattr(value, "to_dict"): + return value.to_dict() + if isinstance(value, Path): + return str(value) + if isinstance(value, Mapping): + return {str(key): _to_plain(item_value) for key, item_value in value.items()} + if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + return [_to_plain(item) for item in value] + return value + + +def _compact(values: Mapping[str, Any]) -> dict[str, Any]: + compacted: dict[str, Any] = {} + for key, value in values.items(): + plain = _to_plain(value) + if plain is None or plain == "" or plain == [] or plain == {}: + continue + compacted[key] = plain + return compacted + + +def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + return [_to_plain(item) for item in value] + return [_to_plain(value)] + + +def _artifact_status(path: Any, status: Any = None) -> Any: + if status not in (None, ""): + return status + if path in (None, ""): + return "missing" + try: + return "available" if Path(str(path)).expanduser().exists() else "missing" + except (OSError, ValueError): + return "missing" + + +@dataclass +class StepEvent: + name: Any = "step" + status: Any = "recorded" + reason: Any = None + duration: Any = None + + def to_dict(self) -> dict[str, Any]: + return _compact({ + "name": self.name, + "status": self.status, + "reason": self.reason, + "duration": self.duration, + }) + + +@dataclass +class RetrievalEvent: + query: Any = None + tool: Any = None + hits: Any = None + reason: Any = None + + def to_dict(self) -> dict[str, Any]: + return _compact({ + "query": self.query, + "tool": self.tool, + "hits": self.hits, + "reason": self.reason, + }) + + +@dataclass +class RPGDeltaEvent: + node_id: Any = None + name: Any = None + type: Any = None + path: Any = None + change: Any = None + score: Any = None + + def to_dict(self) -> dict[str, Any]: + return _compact({ + "node_id": self.node_id, + "name": self.name, + "type": self.type, + "path": self.path, + "change": self.change, + "score": self.score, + }) + + +@dataclass +class DepGraphDeltaEvent: + dep_node_id: Any = None + path: Any = None + source_feature: Any = None + change: Any = None + + def to_dict(self) -> dict[str, Any]: + return _compact({ + "dep_node_id": self.dep_node_id, + "path": self.path, + "source_feature": self.source_feature, + "change": self.change, + }) + + +@dataclass +class CodeDeltaEvent: + file: Any = None + change_type: Any = None + before: Any = None + after: Any = None + diff: Any = None + + def to_dict(self) -> dict[str, Any]: + return _compact({ + "file": self.file, + "change_type": self.change_type, + "before": self.before, + "after": self.after, + "diff": self.diff, + }) + + +@dataclass +class VerificationEvent: + name: Any = "verification" + status: Any = None + detail: Any = None + + def to_dict(self) -> dict[str, Any]: + return _compact({ + "name": self.name, + "status": self.status, + "detail": self.detail, + }) + + +@dataclass +class UserDecisionEvent: + decision: Any = None + branch: Any = None + before_state: Any = None + rollback_path: Any = None + confirmed: Any = None + apply_status: Any = None + test_status: Any = None + + def to_dict(self) -> dict[str, Any]: + return _compact({ + "decision": self.decision, + "branch": self.branch, + "before_state": self.before_state, + "rollback_path": self.rollback_path, + "confirmed": self.confirmed, + "apply_status": self.apply_status, + "test_status": self.test_status, + }) + + +@dataclass +class ArtifactEvent: + label: Any = "artifact" + path: Any = None + status: Any = None + + def to_dict(self) -> dict[str, Any]: + return _compact({ + "label": self.label, + "path": self.path, + "status": _artifact_status(self.path, self.status), + }) + + +@dataclass +class CommandRun: + command: str = "command" + status: Any = None + title: Any = None + timestamp: Any = None + summary: Any = None + steps: Any = None + retrievals: Any = None + rpg_deltas: Any = None + dep_graph_deltas: Any = None + code_deltas: Any = None + verification: Any = None + user_decisions: Any = None + artifacts: Any = None + evidence: Any = None + + def to_dict(self) -> dict[str, Any]: + data: dict[str, Any] = { + "command": self.command, + "status": _to_plain(self.status), + "title": _to_plain(self.title), + "timestamp": _to_plain(self.timestamp), + "summary": _as_list(self.summary), + "steps": _as_list(self.steps), + "retrievals": _as_list(self.retrievals), + "rpg_deltas": _as_list(self.rpg_deltas), + "dep_graph_deltas": _as_list(self.dep_graph_deltas), + "code_deltas": _as_list(self.code_deltas), + "verification": _as_list(self.verification), + "user_decisions": _as_list(self.user_decisions), + "artifacts": _as_list(self.artifacts), + "evidence": _to_plain(self.evidence) if self.evidence is not None else {}, + } + return _compact(data) + + +__all__ = [ + "CommandRun", + "StepEvent", + "RetrievalEvent", + "RPGDeltaEvent", + "DepGraphDeltaEvent", + "CodeDeltaEvent", + "VerificationEvent", + "UserDecisionEvent", + "ArtifactEvent", + "_compact", + "_to_plain", + "_as_list", + "_artifact_status", +] diff --git a/CoderMind/scripts/common/run_report.py b/CoderMind/scripts/common/run_report.py new file mode 100644 index 0000000..65ce20d --- /dev/null +++ b/CoderMind/scripts/common/run_report.py @@ -0,0 +1,2351 @@ +"""Shared HTML renderer for CoderMind command run reports.""" + +from __future__ import annotations + +import json +import re +from datetime import datetime, timezone +from html import escape +from pathlib import Path +from typing import Any, Mapping, Sequence +from urllib.parse import quote + +from common.paths import REPORTS_DIR +from common.run_events import _to_plain + +_MAX_SUMMARY_CARDS = 7 +_D3_ASSET = Path(__file__).resolve().parent / "assets" / "d3.v7.min.js" + + +def write_command_report( + run: Any, + *, + report_dir: str | Path | None = None, + timestamp: str | datetime | None = None, +) -> Path: + """Write a sanitized Explain View HTML report and return its path.""" + if hasattr(run, "to_dict"): + data = run.to_dict() + elif isinstance(run, Mapping): + data = dict(run) + else: + raise TypeError("write_command_report() expects a CommandRun or mapping") + + data = _to_plain(data) + if not isinstance(data, Mapping): + raise TypeError("CommandRun.to_dict() must return a mapping") + + command = str(data.get("command") or "command") + status = data.get("status") + title = data.get("title") + timestamp = timestamp if timestamp is not None else data.get("timestamp") + report_dir = report_dir if report_dir is not None else data.get("report_dir") + + generated_at = _display_timestamp(timestamp) + filename_ts = _filename_timestamp(timestamp) + safe_command = _slug(command) + target_dir = Path(report_dir) if report_dir is not None else REPORTS_DIR + target_dir.mkdir(parents=True, exist_ok=True) + report_path = _unique_report_path(target_dir / f"cmind_run_{safe_command}_{filename_ts}.html") + + evidence = dict(data) + evidence_data = evidence.get("evidence") if isinstance(evidence.get("evidence"), Mapping) else {} + retrievals = data.get("retrievals") or evidence_data.get("retrievals") + code_deltas = data.get("code_deltas") + focused_view = data.get("focused_view") + user_decisions = data.get("user_decisions") or evidence_data.get("user_decisions") + + page_title = title or f"CoderMind {command} Explain View" + html = _render_page( + title=page_title, + command=command, + generated_at=generated_at, + status=status, + summary_cards=_normalize_cards(data.get("summary")), + stages=_normalize_stages(data.get("steps")), + retrievals=_normalize_retrievals(retrievals), + rpg_nodes=_normalize_nodes(data.get("rpg_deltas"), dep_graph=False), + dep_nodes=_normalize_nodes(data.get("dep_graph_deltas"), dep_graph=True), + code_deltas=_normalize_code_deltas(code_deltas), + focused_view=_normalize_focused_view(focused_view), + artifacts=_normalize_artifacts(data.get("artifacts")), + verification=_normalize_verification(data.get("verification")), + user_decisions=_normalize_user_decisions(user_decisions), + evidence=evidence, + ) + report_path.write_text(html, encoding="utf-8") + return report_path + + +def _slug(value: str) -> str: + slug = re.sub(r"[^A-Za-z0-9_.-]+", "_", value).strip("._-") + return slug or "command" + + +def _unique_report_path(path: Path) -> Path: + if not path.exists(): + return path + stem = path.stem + suffix = path.suffix + for index in range(2, 1000): + candidate = path.with_name(f"{stem}_{index}{suffix}") + if not candidate.exists(): + return candidate + return path.with_name(f"{stem}_{datetime.now(timezone.utc).strftime('%f')}{suffix}") + + +def _display_timestamp(value: str | datetime | None) -> str: + if isinstance(value, datetime): + return value.astimezone(timezone.utc).isoformat(timespec="seconds") + if value is not None: + return str(value) + return datetime.now(timezone.utc).isoformat(timespec="seconds") + + +def _filename_timestamp(value: str | datetime | None) -> str: + if isinstance(value, datetime): + raw = value.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + elif value is not None: + raw = str(value) + else: + raw = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + return _slug(raw) + + +def _as_sequence(value: Any) -> list[Any]: + if value is None: + return [] + if isinstance(value, (str, bytes, Path)): + return [value] + if isinstance(value, Mapping): + return [value] + if isinstance(value, Sequence): + return list(value) + return [value] + + +def _normalize_cards(value: Any) -> list[dict[str, Any]]: + cards: list[dict[str, Any]] = [] + for item in _as_sequence(value): + if isinstance(item, Mapping): + cards.append({ + "label": item.get("label") or "Summary", + "value": item.get("value", ""), + "detail": item.get("detail"), + }) + else: + cards.append({"label": "Summary", "value": item}) + return cards[:_MAX_SUMMARY_CARDS] + + +def _normalize_stages(value: Any) -> list[dict[str, Any]]: + stages: list[dict[str, Any]] = [] + for item in _as_sequence(value): + if isinstance(item, Mapping): + stages.append({ + "name": item.get("name") or "stage", + "status": item.get("status", "recorded"), + "reason": item.get("reason", ""), + "duration": item.get("duration"), + }) + else: + stages.append({"name": item, "status": "recorded"}) + return stages + + +def _normalize_retrievals(value: Any) -> list[dict[str, Any]]: + retrievals: list[dict[str, Any]] = [] + for item in _as_sequence(value): + if isinstance(item, Mapping): + hits = item.get("hits") + retrievals.append({ + "query": item.get("query", ""), + "tool": item.get("tool", ""), + "reason": item.get("reason", ""), + "hits": _as_sequence(hits), + }) + else: + retrievals.append({"query": item, "hits": []}) + return retrievals + + +def _normalize_nodes(value: Any, *, dep_graph: bool = False) -> list[dict[str, Any]]: + nodes: list[dict[str, Any]] = [] + id_key = "dep_node_id" if dep_graph else "node_id" + for item in _as_sequence(value): + if isinstance(item, Mapping): + entry = dict(item) + else: + entry = {id_key: item} + nodes.append(entry) + return nodes + + +def _normalize_code_deltas(value: Any) -> list[dict[str, Any]]: + deltas: list[dict[str, Any]] = [] + for item in _as_sequence(value): + if isinstance(item, Mapping): + deltas.append({ + "file": item.get("file") or item.get("path") or "", + "change_type": item.get("change_type") or item.get("status") or "", + "before": item.get("before"), + "after": item.get("after"), + "diff": item.get("diff", ""), + }) + else: + deltas.append({"file": item, "change_type": "recorded", "diff": ""}) + return deltas + + +def _normalize_focused_view(value: Any) -> dict[str, Any]: + if value in (None, "", [], {}): + return {} + if not isinstance(value, Mapping): + return {"detail": value} + normalized = dict(value) + normalized["summary"] = dict(value.get("summary") or {}) if isinstance(value.get("summary"), Mapping) else {} + normalized["primary_rpg_nodes"] = [ + dict(node) if isinstance(node, Mapping) else {"node_id": node} + for node in _as_sequence(value.get("primary_rpg_nodes")) + ] + normalized["primary_code_nodes"] = [ + dict(node) if isinstance(node, Mapping) else {"node_id": node} + for node in _as_sequence(value.get("primary_code_nodes")) + ] + normalized["mappings"] = [ + dict(mapping) if isinstance(mapping, Mapping) else {"detail": mapping} + for mapping in _as_sequence(value.get("mappings")) + ] + normalized["edges"] = [ + dict(edge) if isinstance(edge, Mapping) else {"detail": edge} + for edge in _as_sequence(value.get("edges")) + ] + normalized["hidden_counts"] = dict(value.get("hidden_counts") or {}) if isinstance(value.get("hidden_counts"), Mapping) else {} + normalized["hidden_context_nodes"] = [ + dict(node) if isinstance(node, Mapping) else {"detail": node} + for node in _as_sequence(value.get("hidden_context_nodes")) + ] + normalized["warnings"] = [ + dict(warning) if isinstance(warning, Mapping) else {"message": warning} + for warning in _as_sequence(value.get("warnings")) + ] + nodes_view = value.get("nodes_view") + if isinstance(nodes_view, Mapping): + normalized["nodes_view"] = _normalize_nodes_view(nodes_view) + normalized["unmatched_code_deltas"] = [ + dict(delta) if isinstance(delta, Mapping) else {"file": delta} + for delta in _as_sequence(value.get("unmatched_code_deltas")) + ] + return normalized + + +def _normalize_nodes_view(value: Mapping[str, Any]) -> dict[str, Any]: + normalized = dict(value) + normalized["summary"] = dict(value.get("summary") or {}) if isinstance(value.get("summary"), Mapping) else {} + for key in ("semantic_nodes", "code_nodes", "mappings", "edges", "warnings", "changed_files", "hidden_context_nodes"): + normalized[key] = [ + dict(item) if isinstance(item, Mapping) else {"detail": item} + for item in _as_sequence(value.get(key)) + ] + normalized["hidden_counts"] = dict(value.get("hidden_counts") or {}) if isinstance(value.get("hidden_counts"), Mapping) else {} + return normalized + + +def _normalize_artifacts(value: Any) -> list[dict[str, Any]]: + artifacts: list[dict[str, Any]] = [] + for item in _as_sequence(value): + if isinstance(item, Mapping): + path = item.get("path") + artifacts.append({ + "label": item.get("label") or path or "artifact", + "path": path, + "status": _artifact_status(path, item.get("status")), + "detail": item.get("detail"), + }) + else: + artifacts.append({"label": Path(str(item)).name or "artifact", "path": item, "status": _artifact_status(item)}) + return artifacts + + +def _artifact_status(path: Any, status: Any = None) -> Any: + if status not in (None, ""): + return status + if path in (None, ""): + return "missing" + try: + return "available" if Path(str(path)).expanduser().exists() else "missing" + except (OSError, ValueError): + return "missing" + + +def _normalize_verification(value: Any) -> list[dict[str, Any]]: + checks: list[dict[str, Any]] = [] + for item in _as_sequence(value): + if isinstance(item, Mapping): + checks.append({ + "name": item.get("name") or "verification", + "status": item.get("status", ""), + "detail": item.get("detail"), + }) + else: + checks.append({"name": "verification", "status": item}) + return checks + + +def _normalize_user_decisions(value: Any) -> list[dict[str, Any]]: + decisions: list[dict[str, Any]] = [] + for item in _as_sequence(value): + if isinstance(item, Mapping): + entry = dict(item) + else: + entry = {"decision": item} + decisions.append({ + "decision": entry.get("decision", ""), + "before_state": entry.get("before_state"), + "confirmed": entry.get("confirmed"), + "branch": entry.get("branch", ""), + "apply_status": entry.get("apply_status", ""), + "test_status": entry.get("test_status", ""), + "rollback_path": entry.get("rollback_path", ""), + }) + return decisions + + +def _render_page( + *, + title: str, + command: str, + generated_at: str, + status: str | None, + summary_cards: list[dict[str, Any]], + stages: list[dict[str, Any]], + retrievals: list[dict[str, Any]], + rpg_nodes: list[dict[str, Any]], + dep_nodes: list[dict[str, Any]], + code_deltas: list[dict[str, Any]], + focused_view: dict[str, Any], + artifacts: list[dict[str, Any]], + verification: list[dict[str, Any]], + user_decisions: list[dict[str, Any]], + evidence: Mapping[str, Any], +) -> str: + status_html = f"{_h(status)}" if status else "" + code_delta_anchors = _code_delta_anchors(code_deltas) + code_file_anchors = _code_file_anchor_map(code_deltas, code_delta_anchors) + focused_graph_html = _render_focused_graph(focused_view, code_file_anchors) + code_deltas_html = _render_code_deltas(code_deltas, code_delta_anchors) + summary_html = _render_summary_cards(summary_cards) + timeline_html = _render_timeline(stages, verification) + safety_html = _render_safety_boundary(user_decisions) + if focused_graph_html: + primary_sections_html = summary_html + focused_graph_html + code_deltas_html + timeline_html + safety_html + else: + primary_sections_html = summary_html + timeline_html + safety_html + code_deltas_html + return f""" + + + + +{_h(title)} + + + +
+
+

{_h(title)}

+
Command: {_h(command)}Generated: {_h(generated_at)}{status_html}
+
+{primary_sections_html} +{_render_artifacts(artifacts)} +{_render_evidence(evidence)} +
+ + +""" + + +def _render_summary_cards(cards: list[dict[str, Any]]) -> str: + if not cards: + body = "

No summary cards recorded.

" + else: + rendered_cards = [] + for card in cards: + detail = card.get("detail") + detail_html = "" + if detail is not None: + detail_html = f"
{_h(detail)}
" + value = card.get("value", "") + long_value = len(str(value)) > 48 + card_class = "card card-wide" if long_value else "card" + value_class = "card-value card-value-long" if long_value else "card-value" + rendered_cards.append( + f"
{_h(card.get('label', 'Summary'))}
" + f"
{_h(value)}
" + f"{detail_html}
" + ) + body = '
' + "".join(rendered_cards) + "
" + return f"

Summary

{body}
" + + +def _render_timeline(stages: list[dict[str, Any]], checks: list[dict[str, Any]]) -> str: + items = [] + for stage in stages: + duration = stage.get("duration") + duration_text = f"{_h(duration)}s" if duration not in (None, "") else "" + items.append( + "
  • " + f"
    {_h(stage.get('name', 'stage'))}" + f"{_h(stage.get('status', 'recorded'))}{duration_text}
    " + f"
    {_h(stage.get('reason', ''))}
    " + "
  • " + ) + for check in checks: + items.append( + "
  • " + f"
    {_h(check.get('name') or 'verification')}" + f"{_h(check.get('status', ''))}
    " + f"
    {_h(check.get('detail', ''))}
    " + "
  • " + ) + if items: + body = '
      ' + "".join(items) + "
    " + else: + body = "

    No stages recorded.

    " + return f"

    Stage timeline

    {body}
    " + + +def _render_safety_boundary(decisions: list[dict[str, Any]]) -> str: + if not decisions: + return "" + rows = [] + for decision in decisions: + confirmed = decision.get("confirmed") + if confirmed is True: + confirmation = "confirmed" + elif confirmed is False: + confirmation = "not confirmed" + else: + confirmation = "" + rows.append( + "" + f"{_h(decision.get('decision', ''))}" + f"{_h(decision.get('before_state'))}" + f"{_h(confirmation)}" + f"{_h(decision.get('branch', ''))}" + f"{_h(decision.get('apply_status', ''))}" + f"{_h(decision.get('test_status', ''))}" + f"{_h(decision.get('rollback_path', ''))}" + "" + ) + body = ( + "" + "" + "" + + "".join(rows) + + "
    DecisionBefore stateConfirmationBranchApply statusTest statusRollback path
    " + ) + return f"

    Safety boundary

    {body}
    " + + +def _render_retrievals(retrievals: list[dict[str, Any]], *, title: str = "Retrieval evidence", as_section: bool = True) -> str: + if not retrievals: + return "" if as_section else f"

    {_h(title)}

    No retrieval evidence recorded.

    " + rows = [] + for retrieval in retrievals: + hits = retrieval.get("hits") or [] + hit_items = [] + for hit in hits: + if isinstance(hit, Mapping): + label = hit.get("node_id") or hit.get("dep_node_id") or hit.get("path") or hit.get("file") or hit.get("name") or "hit" + reason = hit.get("reason") or hit.get("score") or hit.get("status") or "" + hit_items.append(f"
  • {_h(label)} {_h(reason)}
  • ") + else: + hit_items.append(f"
  • {_h(hit)}
  • ") + hits_html = "No hits recorded." + if hit_items: + hits_html = '
      ' + "".join(hit_items) + "
    " + rows.append( + "" + f"{_h(retrieval.get('tool', ''))}" + f"{_h(retrieval.get('query', ''))}" + f"{_h(retrieval.get('reason', ''))}" + f"{hits_html}" + "" + ) + body = "" + "".join(rows) + "
    ToolQueryReasonHits
    " + if not as_section: + return f"

    {_h(title)}

    {body}" + return f"

    {_h(title)}

    {body}
    " + + +def _delta_file(delta: Mapping[str, Any]) -> str: + return str(delta.get("file") or delta.get("path") or "") + + +def _code_delta_anchors(deltas: list[dict[str, Any]]) -> list[str]: + anchors: list[str] = [] + used: dict[str, int] = {} + for index, delta in enumerate(deltas, start=1): + file_path = _delta_file(delta) or f"change-{index}" + base = _slug(f"diff-{file_path}") + count = used.get(base, 0) + 1 + used[base] = count + anchors.append(base if count == 1 else f"{base}-{count}") + return anchors + + +def _code_file_anchor_map(deltas: list[dict[str, Any]], anchors: list[str]) -> dict[str, str]: + file_anchors: dict[str, str] = {} + for delta, anchor in zip(deltas, anchors): + file_path = _delta_file(delta) + if file_path and file_path not in file_anchors: + file_anchors[file_path] = anchor + return file_anchors + + +def _diff_file_link(file_path: Any, file_anchors: Mapping[str, str]) -> str: + file_text = str(file_path or "") + anchor = file_anchors.get(file_text) + if not anchor: + return f"{_h(file_text)}" + return f"{_h(file_text)}" + + +def _render_code_deltas(deltas: list[dict[str, Any]], anchors: list[str] | None = None) -> str: + if not deltas: + return "" + anchors = anchors or _code_delta_anchors(deltas) + blocks = [] + for index, delta in enumerate(deltas): + anchor = anchors[index] if index < len(anchors) else _slug(f"diff-{index + 1}") + diff = delta.get("diff", "") + diff_html = "

    No diff recorded.

    " + if diff: + diff_html = f"
    View diff
    {_h(diff)}
    " + before_after = "" + if delta.get("before") is not None or delta.get("after") is not None: + before_after = ( + "
    Before/after" + f"
    {_h({'before': delta.get('before'), 'after': delta.get('after')})}
    " + "
    " + ) + blocks.append( + f"
    " + "
    " + f"{_h(delta.get('file', ''))}" + f"{_h(delta.get('change_type', ''))}" + "
    " + f"{diff_html}{before_after}" + "
    " + ) + return f"

    What changed?

    {''.join(blocks)}
    " + + +def _summary_badges(summary: Mapping[str, Any], labels: list[tuple[str, str, Any]]) -> str: + badges = [] + for label, key, fallback in labels: + value = summary.get(key, fallback) + badges.append(f"{_h(label)}: {_h(value)}") + return "
    " + "".join(badges) + "
    " + + +def _mapping_changed_files(mapping: Mapping[str, Any], rpg_node: Mapping[str, Any]) -> list[str]: + return _ordered_texts(_as_sequence(mapping.get("changed_files")) + _as_sequence(rpg_node.get("changed_files"))) + + +def _ordered_texts(values: Sequence[Any]) -> list[str]: + seen: set[str] = set() + ordered: list[str] = [] + for value in values: + if value in (None, ""): + continue + text = str(value) + if text in seen: + continue + seen.add(text) + ordered.append(text) + return ordered + + +def _changed_file_links(files: Sequence[Any], file_anchors: Mapping[str, str]) -> str: + file_list = _ordered_texts(list(files)) + if not file_list: + return "No changed files mapped." + return ", ".join(_diff_file_link(file_path, file_anchors) for file_path in file_list) + + +def _chain_warning_html(warnings: Sequence[Mapping[str, Any]]) -> str: + if not warnings: + return "No warnings." + items = [] + for warning in warnings: + warning_type = warning.get("type", "warning") + context = {key: value for key, value in warning.items() if key not in {"type", "message"}} + context_html = f" {_h(context)}" if context else "" + items.append(f"
  • {_h(warning_type)} {_h(warning.get('message', ''))}{context_html}
  • ") + return '
      ' + "".join(items) + "
    " + + +def _chain_edge_html(edges: Sequence[Mapping[str, Any]]) -> str: + if not edges: + return "No visible neighborhood edges." + items = [] + for edge in edges: + source = edge.get("source_node_id", "") + target = edge.get("target_node_id", "") + relation = edge.get("relation") or "dependency" + direction = edge.get("direction", "") + path = edge.get("path", "") + reason = edge.get("reason", "") + items.append( + "
  • " + f"{_h(source)}{_h(target)}" + f" {_h(relation)}" + f" {_h(direction)}" + f"
    {_h(path)} {_h(reason)}
    " + "
  • " + ) + return '
      ' + "".join(items) + "
    " + + +def _combined_hidden_counts(hidden_counts: Mapping[str, Any]) -> list[tuple[str, int]]: + relation_keys = {"caller": "callers", "callee": "callees", "import": "imports", "inheritance": "inheritance"} + rows: list[tuple[str, int]] = [] + for relation, count_key in relation_keys.items(): + count = hidden_counts.get(count_key) or 0 + try: + count_int = int(count) + except (TypeError, ValueError): + count_int = 0 + if count_int: + rows.append((relation, count_int)) + return rows + + +def _hidden_context_html(hidden_counts: Mapping[str, Any], hidden_context_nodes: Sequence[Any] = ()) -> str: + parts = [] + for relation, count in _combined_hidden_counts(hidden_counts): + parts.append(f"

    Hidden {_h(count)} additional {_h(relation)} neighbors.

    ") + rows = [node for node in _as_sequence(hidden_context_nodes) if isinstance(node, Mapping)] + if rows: + items = [] + for node in rows: + node_id = node.get("node_id") or node.get("rpg_node_id") or node.get("id") or "hidden context" + title = node.get("name") or node.get("symbol") or node_id + reason = node.get("hidden_reason") or node.get("reason") or "hidden context" + path = node.get("breadcrumb_path") or node.get("feature_path") or node.get("path") or "" + items.append( + "
  • " + f"{_h(node_id)} {_h(title)}" + f" hidden context" + f"
    {_h(reason)}
    " + f"
    {_h(path)}
    " + "
  • " + ) + parts.append("
    Hidden context nodes
      " + "".join(items) + "
    ") + return "".join(parts) + + +def _focused_inspector_payload(focused_view: Mapping[str, Any]) -> dict[str, Any]: + nodes_view = focused_view.get("nodes_view") if isinstance(focused_view.get("nodes_view"), Mapping) else {} + if nodes_view: + return {"nodes_view": nodes_view} + payload: dict[str, Any] = {} + for key, value in focused_view.items(): + if key in {"unmatched_code_deltas"}: + unmatched_files = [ + _delta_file(delta) + for delta in _as_sequence(value) + if isinstance(delta, Mapping) and _delta_file(delta) + ] + if unmatched_files: + payload["unmatched_changed_files"] = unmatched_files + continue + payload[key] = value + return payload + + +def _node_view_id(node: Mapping[str, Any]) -> str: + return str(node.get("link_id") or node.get("node_id") or node.get("dep_node_id") or "") + + +def _line_range_text(node: Mapping[str, Any]) -> str: + line_range = node.get("line_range") if isinstance(node.get("line_range"), Mapping) else {} + start = line_range.get("start") or node.get("line_start") or node.get("start_line") or node.get("lineno") or node.get("line") + end = line_range.get("end") or node.get("line_end") or node.get("end_line") or start + if start in (None, ""): + return "unavailable" + if end in (None, "") or str(end) == str(start): + return str(start) + return f"{start}-{end}" + + +def _changed_refs_html(refs: Sequence[Any], file_anchors: Mapping[str, str]) -> str: + links = [] + for ref in _as_sequence(refs): + if isinstance(ref, Mapping): + path = ref.get("path") or ref.get("file") + anchor = ref.get("diff_anchor") or file_anchors.get(str(path or "")) + else: + path = ref + anchor = file_anchors.get(str(path or "")) + if path in (None, ""): + continue + if anchor: + links.append(f"{_h(path)}") + else: + links.append(f"{_h(path)}") + if not links: + return "" + return '" + + +def _focus_card_badges(node: Mapping[str, Any], *keys: str) -> str: + badges = [] + for key in keys: + value = node.get(key) + if value not in (None, "", [], {}): + badges.append(f"{_h(value)}") + return "".join(badges) + + +def _semantic_card(node: Mapping[str, Any], file_anchors: Mapping[str, str]) -> str: + node_id = node.get("node_id") + link_id = _node_view_id(node) + title = node.get("name") or node.get("symbol") or node_id or "semantic node" + breadcrumb = node.get("breadcrumb_path") or node.get("breadcrumb") or node.get("feature_path") or node.get("path") or "unavailable" + mapped = _as_sequence(node.get("mapped_code_node_ids")) + mapped_html = "" + if mapped: + mapped_html = '
    Mapped code: ' + ", ".join(f"{_h(item)}" for item in mapped) + "
    " + elif (node.get("mapping_status") or node.get("state")) in {"missing", "missing_mapping"}: + mapped_html = '
    Mapped code: missing mapping
    ' + warnings = _as_sequence(node.get("warning_types")) + warning_html = '
    Warnings: ' + ", ".join(f"{_h(item)}" for item in warnings) + "
    " if warnings else "" + return ( + f"" + ) + + +def _code_card(node: Mapping[str, Any], file_anchors: Mapping[str, str]) -> str: + node_id = node.get("node_id") or node.get("dep_node_id") + link_id = _node_view_id(node) + path = node.get("path") or node.get("module") or node.get("file") or "unavailable" + symbol = node.get("symbol") or node.get("name") or "unavailable" + changed_refs = _as_sequence(node.get("changed_files")) + if not changed_refs and node.get("diff_anchor") and path not in (None, "", "unavailable"): + changed_refs = [{"path": path, "diff_anchor": node.get("diff_anchor")}] + mapped = _as_sequence(node.get("mapped_rpg_node_ids")) + mapped_html = '
    Mapped features: ' + ", ".join(f"{_h(item)}" for item in mapped) + "
    " if mapped else "" + return ( + f"" + ) + + +def _mapping_card(mapping: Mapping[str, Any]) -> str: + target = mapping.get("code_node_id") or mapping.get("dep_node_id") or "missing mapping" + source_link = mapping.get("source_link_id") + target_link = mapping.get("target_link_id") + source_html = f"{_h(mapping.get('rpg_node_id'))}" if source_link else f"{_h(mapping.get('rpg_node_id'))}" + target_html = f"{_h(target)}" if target_link else f"{_h(target)}" + return ( + "
    " + f"
    mapping{_focus_card_badges(mapping, 'state', 'status', 'source')}
    " + f"
    {source_html} → {target_html}
    " + f"
    {_h(mapping.get('path') or mapping.get('reason') or '')}
    " + "
    " + ) + + +def _inline_d3() -> str: + try: + return _D3_ASSET.read_text(encoding="utf-8").strip() + except OSError: + return "" + + +def _json_for_script(value: Any) -> str: + data = json.dumps(value, ensure_ascii=False, default=_json_default) + return ( + data.replace("&", "\\u0026") + .replace("<", "\\u003c") + .replace(">", "\\u003e") + .replace("
", "\\u2028") + .replace("
", "\\u2029") + ) + + +def _mapped_code_label(row: Mapping[str, Any]) -> str: + path = row.get("mapped_code_path") + symbol = row.get("mapped_code_symbol") + first = next((item for item in _as_sequence(row.get("mapped_code")) if isinstance(item, Mapping)), None) + if first: + path = path or first.get("path") + symbol = symbol or first.get("symbol") or first.get("name") + if not path: + paths = _as_sequence(row.get("mapped_code_paths")) + path = paths[0] if paths else None + if not symbol: + symbols = _as_sequence(row.get("mapped_code_symbols")) + symbol = symbols[0] if symbols else None + detail = " · ".join(str(item) for item in (path, symbol) if item not in (None, "")) + if not detail: + return "" + count = row.get("mapped_code_count") + try: + count_int = int(count) + except (TypeError, ValueError): + count_int = 0 + suffix = f" +{count_int - 1}" if count_int > 1 else "" + return f"{detail}{suffix}" + + +def _graph_label(row: Mapping[str, Any], fallback: Any) -> str: + base = "" + for key in ("feature_name", "name", "symbol", "label", "path", "node_id", "dep_node_id", "id"): + value = row.get(key) + if value not in (None, ""): + base = str(value) + break + if not base: + base = str(fallback or "node") + mapped_code = _mapped_code_label(row) + return f"{base} — {mapped_code}" if mapped_code else base + + +def _graph_diff_ref(row: Mapping[str, Any], file_anchors: Mapping[str, str]) -> dict[str, str]: + refs = _as_sequence(row.get("changed_files")) + path = row.get("path") or row.get("file") or row.get("module") + if not refs and row.get("diff_anchor") and path not in (None, ""): + refs = [{"path": path, "diff_anchor": row.get("diff_anchor")}] + for ref in refs: + if isinstance(ref, Mapping): + ref_path = ref.get("path") or ref.get("file") + anchor = ref.get("diff_anchor") or file_anchors.get(str(ref_path or "")) + else: + ref_path = ref + anchor = file_anchors.get(str(ref_path or "")) + if ref_path not in (None, "") and anchor: + return {"path": str(ref_path), "href": f"#{anchor}"} + return {} + + +def _graph_node_id(row: Mapping[str, Any], kind: str) -> str: + explicit = _node_view_id(row) + if explicit: + return explicit + if kind == "mapping": + node_id = f"{row.get('rpg_node_id') or 'semantic'}-{row.get('code_node_id') or row.get('dep_node_id') or 'missing'}" + else: + node_id = row.get("node_id") or row.get("dep_node_id") or row.get("id") or kind + return f"{kind}-{_slug(str(node_id))}" + + +def _append_graph_node(nodes: list[dict[str, Any]], seen: set[str], row: Mapping[str, Any], kind: str, file_anchors: Mapping[str, str]) -> str: + node_id = _graph_node_id(row, kind) + if node_id in seen: + return node_id + seen.add(node_id) + payload: dict[str, Any] = { + "id": node_id, + "kind": kind, + "label": _graph_label(row, row.get("node_id") or row.get("dep_node_id") or node_id), + "node_id": row.get("node_id") or row.get("dep_node_id"), + "state": row.get("state") or row.get("status") or row.get("mapping_status"), + } + for key in ( + "path", + "feature_path", + "feature_name", + "type", + "node_type", + "source", + "mapping_status", + "locate_status", + "breadcrumb_path", + "mapped_code", + "mapped_code_path", + "mapped_code_paths", + "mapped_code_symbol", + "mapped_code_symbols", + "mapped_code_count", + ): + if row.get(key) not in (None, ""): + payload[key] = row.get(key) + diff_ref = _graph_diff_ref(row, file_anchors) + if diff_ref: + payload["diff"] = diff_ref + nodes.append(payload) + return node_id + + +def _append_context_node(nodes: list[dict[str, Any]], seen: set[str], link_id: Any, node_id: Any) -> str: + graph_id = str(link_id or f"context-{_slug(str(node_id or 'node'))}") + if graph_id not in seen: + seen.add(graph_id) + nodes.append({"id": graph_id, "kind": "context", "label": str(node_id or "context"), "node_id": node_id, "state": "context"}) + return graph_id + + +def _append_hierarchy_nodes( + nodes: list[dict[str, Any]], + links: list[dict[str, Any]], + seen_nodes: set[str], + hierarchy: Any, + file_anchors: Mapping[str, str], + node_metadata_by_id: Mapping[str, Mapping[str, Any]], +) -> None: + if not isinstance(hierarchy, Mapping): + return + seen_links = {str(link.get("id")) for link in links if isinstance(link, Mapping) and link.get("id") not in (None, "")} + + def node_id(row: Mapping[str, Any]) -> str: + return str(row.get("id") or row.get("link_id") or row.get("node_id") or "") + + def visit(row: Any) -> str: + if not isinstance(row, Mapping): + return "" + row_id = node_id(row) + if row_id and row_id not in seen_nodes: + seen_nodes.add(row_id) + metadata = node_metadata_by_id.get(row_id) or node_metadata_by_id.get(str(row.get("node_id") or "")) or {} + payload = { + "id": row_id, + "kind": row.get("kind") or "feature", + "label": _graph_label(row, row.get("node_id") or row_id), + "node_id": row.get("node_id") or row_id, + "state": row.get("state") or row.get("kind") or "hierarchy", + "type": row.get("kind") or "hierarchy", + } + for key in ( + "name", + "feature_name", + "feature_path", + "path", + "module", + "file", + "symbol", + "dep_node_id", + "type", + "node_type", + "signature", + "mapping_status", + "locate_status", + "breadcrumb", + "breadcrumb_path", + "score", + "source", + "source_graph", + "edge_source", + "relation_source", + "source_feature", + "source_features", + "relation", + "direction", + "reason", + "line_range", + "apply_action", + "mapped_code", + "mapped_code_node_ids", + "mapped_code_link_ids", + "mapped_code_path", + "mapped_code_paths", + "mapped_code_symbol", + "mapped_code_symbols", + "mapped_code_count", + "mapped_rpg_node_ids", + "mapped_rpg_link_ids", + "changed", + "changed_files", + "affected_files", + "diff_anchor", + "diff", + "hidden_counts", + "warning_types", + "rpg_node_id", + "neighbor_node_id", + ): + value = row.get(key) if row.get(key) not in (None, "") else metadata.get(key) + if value not in (None, ""): + payload[key] = value + diff_ref = _graph_diff_ref(payload, file_anchors) + if diff_ref: + payload["diff"] = diff_ref + nodes.append(payload) + for child in [child for child in _as_sequence(row.get("children")) if isinstance(child, Mapping)]: + child_id = visit(child) + if row_id in seen_nodes and child_id in seen_nodes: + link_id = f"hierarchy-{_slug(row_id)}-{_slug(child_id)}" + if link_id not in seen_links: + seen_links.add(link_id) + links.append({"id": link_id, "source": row_id, "target": child_id, "kind": "hierarchy", "relation": "contains"}) + return row_id + + visit(hierarchy) + + +def _focused_graph_payload(focused_view: Mapping[str, Any], file_anchors: Mapping[str, str]) -> dict[str, Any]: + nodes_view = focused_view.get("nodes_view") if isinstance(focused_view.get("nodes_view"), Mapping) else {} + summary_source = nodes_view.get("summary") if isinstance(nodes_view.get("summary"), Mapping) else focused_view.get("summary", {}) + summary = dict(summary_source) if isinstance(summary_source, Mapping) else {} + semantic_nodes = [node for node in _as_sequence(nodes_view.get("semantic_nodes")) if isinstance(node, Mapping)] + code_nodes = [node for node in _as_sequence(nodes_view.get("code_nodes")) if isinstance(node, Mapping)] + mappings = [mapping for mapping in _as_sequence(nodes_view.get("mappings")) if isinstance(mapping, Mapping)] + context_edges = [edge for edge in _as_sequence(nodes_view.get("edges")) if isinstance(edge, Mapping)] + hidden_counts = nodes_view.get("hidden_counts") if isinstance(nodes_view.get("hidden_counts"), Mapping) else focused_view.get("hidden_counts", {}) + hidden_context_nodes = [node for node in _as_sequence(nodes_view.get("hidden_context_nodes") or focused_view.get("hidden_context_nodes")) if isinstance(node, Mapping)] + warnings = [warning for warning in _as_sequence(nodes_view.get("warnings")) if isinstance(warning, Mapping)] + focused_graph = nodes_view.get("focused_graph") if isinstance(nodes_view.get("focused_graph"), Mapping) else {} + hierarchy = nodes_view.get("hierarchy") or focused_graph.get("hierarchy") or {} + default_focus = nodes_view.get("default_focus") or focused_graph.get("default_focus") or {} + + if not isinstance(hierarchy, Mapping) or not hierarchy or (semantic_nodes and not _as_sequence(hierarchy.get("children"))): + hierarchy = { + "id": "focused-graph-root", + "name": "Focused graph", + "kind": "root", + "children": [ + { + "id": _graph_node_id(node, "feature"), + "node_id": node.get("node_id"), + "name": node.get("name") or node.get("symbol") or node.get("node_id") or "feature", + "kind": "feature", + "feature_name": node.get("name") or node.get("symbol") or node.get("node_id") or "feature", + "feature_path": node.get("breadcrumb_path") or node.get("feature_path"), + "mapped_code": node.get("mapped_code"), + "mapped_code_node_ids": node.get("mapped_code_node_ids"), + "mapped_code_link_ids": node.get("mapped_code_link_ids"), + "mapped_code_path": node.get("mapped_code_path"), + "mapped_code_paths": node.get("mapped_code_paths"), + "mapped_code_symbol": node.get("mapped_code_symbol"), + "mapped_code_symbols": node.get("mapped_code_symbols"), + "mapped_code_count": node.get("mapped_code_count"), + } + for node in semantic_nodes + ], + } + + semantic_link_by_node = { + str(node.get("node_id")): str(node.get("link_id") or _graph_node_id(node, "feature")) + for node in semantic_nodes + if node.get("node_id") not in (None, "") + } + code_link_by_node = { + str(node.get("node_id") or node.get("dep_node_id")): str(node.get("link_id") or _graph_node_id(node, "code")) + for node in code_nodes + if (node.get("node_id") or node.get("dep_node_id")) not in (None, "") + } + node_metadata_by_id: dict[str, Mapping[str, Any]] = {} + + def remember_node_metadata(alias: Any, row: Mapping[str, Any]) -> None: + if alias in (None, ""): + return + node_metadata_by_id.setdefault(str(alias), row) + + for node in semantic_nodes: + node_id = node.get("node_id") + remember_node_metadata(node.get("link_id") or _graph_node_id(node, "feature"), node) + remember_node_metadata(node_id, node) + for node in code_nodes: + node_id = node.get("node_id") or node.get("dep_node_id") + remember_node_metadata(node.get("link_id") or _graph_node_id(node, "code"), node) + remember_node_metadata(node_id, node) + + def endpoint_link_id(edge: Mapping[str, Any], side: str) -> str: + node_id = edge.get(f"{side}_node_id") + node_text = str(node_id or "") + return str( + edge.get(f"{side}_link_id") + or semantic_link_by_node.get(node_text) + or code_link_by_node.get(node_text) + or _graph_node_id({"node_id": node_text}, "context") + ) + + def collect_hierarchy_ids(row: Any, ids: set[str]) -> None: + if not isinstance(row, Mapping): + return + row_id = row.get("id") or row.get("link_id") or row.get("node_id") + if row_id not in (None, ""): + ids.add(str(row_id)) + for child in _as_sequence(row.get("children")): + collect_hierarchy_ids(child, ids) + + def endpoint_group() -> dict[str, Any]: + children = hierarchy.setdefault("children", []) if isinstance(hierarchy, dict) else [] + group_id = "focused-graph-relation-endpoints" + for child in children: + if isinstance(child, dict) and child.get("id") == group_id: + return child + group = {"id": group_id, "name": "Relation endpoints", "kind": "context_group", "children": []} + children.append(group) + return group + + hierarchy_ids: set[str] = set() + collect_hierarchy_ids(hierarchy, hierarchy_ids) + code_link_values = set(code_link_by_node.values()) + for edge in context_edges: + for side in ("source", "target"): + link_id = endpoint_link_id(edge, side) + if not link_id or link_id in hierarchy_ids: + continue + node_id = edge.get(f"{side}_node_id") + node_text = str(node_id or link_id) + is_code = link_id in code_link_values + leaf: dict[str, Any] = { + "id": link_id, + "node_id": node_text, + "name": edge.get(f"{side}_name") or edge.get("name") or edge.get(f"{side}_path") or edge.get("path") or node_text, + "kind": "code" if is_code else "context", + "state": "mapped" if is_code else "context", + "aliases": _ordered_texts([node_text, link_id]), + } + if is_code: + code = next((row for row in code_nodes if str(row.get("node_id") or row.get("dep_node_id") or "") == node_text), {}) + for key in ("path", "symbol", "type", "line_range", "source", "changed_files", "diff_anchor"): + if isinstance(code, Mapping) and code.get(key) not in (None, ""): + leaf[key] = code.get(key) + else: + for key in ("relation", "direction", "reason", "source", "source_graph", "edge_source", "relation_source"): + if edge.get(key) not in (None, ""): + leaf[key] = edge.get(key) + path = edge.get(f"{side}_path") or edge.get("path") + if path not in (None, ""): + leaf["path"] = path + endpoint_group().setdefault("children", []).append(leaf) + hierarchy_ids.add(link_id) + + nodes: list[dict[str, Any]] = [] + links: list[dict[str, Any]] = [] + seen_nodes: set[str] = set() + _append_hierarchy_nodes(nodes, links, seen_nodes, hierarchy, file_anchors, node_metadata_by_id) + + rpg_links: dict[str, str] = {} + code_links: dict[str, str] = {} + node_aliases: dict[str, list[str]] = {} + + def add_alias(alias: Any, target: Any) -> None: + if alias in (None, "") or target in (None, ""): + return + alias_text = str(alias) + target_text = str(target) + values = node_aliases.setdefault(alias_text, []) + if target_text not in values: + values.append(target_text) + + def visit_hierarchy(row: Any) -> None: + if not isinstance(row, Mapping): + return + row_id = str(row.get("id") or row.get("link_id") or row.get("node_id") or "") + node_id = row.get("node_id") + kind = str(row.get("kind") or "") + if node_id not in (None, "") and row_id: + if kind in {"code", "context"}: + add_alias(node_id, row_id) + add_alias(row_id, row_id) + else: + rpg_links.setdefault(str(node_id), row_id) + for alias in _as_sequence(row.get("aliases")): + add_alias(alias, row_id) + for code_id in _as_sequence(row.get("mapped_code_node_ids")): + add_alias(_graph_node_id({"node_id": code_id}, "code"), row_id) + add_alias(code_id, row_id) + for code_link in _as_sequence(row.get("mapped_code_link_ids")): + add_alias(code_link, row_id) + for code_ref in _as_sequence(row.get("mapped_code")): + if isinstance(code_ref, Mapping): + add_alias(code_ref.get("link_id"), row_id) + add_alias(code_ref.get("node_id") or code_ref.get("dep_node_id"), row_id) + for child in _as_sequence(row.get("children")): + visit_hierarchy(child) + + visit_hierarchy(hierarchy) + + for node in semantic_nodes: + link_id = str(node.get("link_id") or _graph_node_id(node, "feature")) + node_id = node.get("node_id") + if node_id not in (None, ""): + rpg_links.setdefault(str(node_id), link_id) + for node in code_nodes: + link_id = str(node.get("link_id") or _graph_node_id(node, "code")) + node_id = node.get("node_id") or node.get("dep_node_id") + if node_id not in (None, ""): + code_links[str(node_id)] = link_id + for rpg_link in _as_sequence(node.get("mapped_rpg_link_ids")): + add_alias(link_id, rpg_link) + add_alias(node_id, rpg_link) + + for mapping in mappings: + source = mapping.get("source_link_id") or rpg_links.get(str(mapping.get("rpg_node_id") or mapping.get("node_id") or "")) + target = mapping.get("target_link_id") or code_links.get(str(mapping.get("code_node_id") or mapping.get("dep_node_id") or "")) + add_alias(target, source) + add_alias(mapping.get("code_node_id") or mapping.get("dep_node_id"), source) + + def edge_kind(edge: Mapping[str, Any]) -> str: + source_parts = " ".join( + str(edge.get(key) or "") + for key in ("source_graph", "edge_source", "relation_source", "source") + ).lower() + relation = str(edge.get("relation") or "").lower() + if "rpg" in source_parts or relation in {"semantic", "depends_on", "related", "relates_to"}: + return "semantic" + return "dependency" + + def candidates(edge: Mapping[str, Any], side: str) -> list[str]: + values: list[Any] = [] + values.extend(_as_sequence(edge.get(f"{side}_candidates"))) + values.extend(_as_sequence(edge.get(f"{side}_rpg_link_ids"))) + values.append(edge.get(f"{side}_link_id")) + node_id = edge.get(f"{side}_node_id") + values.append(rpg_links.get(str(node_id or ""))) + values.append(code_links.get(str(node_id or ""))) + values.append(node_id) + expanded: list[str] = [] + for value in values: + if value in (None, ""): + continue + text = str(value) + expanded.append(text) + expanded.extend(node_aliases.get(text, [])) + return _ordered_texts(expanded) + + relation_edges: list[dict[str, Any]] = [] + for index, edge in enumerate(context_edges, start=1): + source_candidates = candidates(edge, "source") + target_candidates = candidates(edge, "target") + relation_edges.append({ + "id": str(edge.get("link_id") or f"edge-{index}"), + "source": source_candidates[0] if source_candidates else "", + "target": target_candidates[0] if target_candidates else "", + "source_candidates": source_candidates, + "target_candidates": target_candidates, + "kind": edge_kind(edge), + "relation": edge.get("relation") or "dependency", + "source_meta": edge.get("source"), + "source_graph": edge.get("source_graph"), + "edge_source": edge.get("edge_source"), + "relation_source": edge.get("relation_source"), + "direction": edge.get("direction"), + "reason": edge.get("reason"), + "path": edge.get("path"), + }) + + summary.update({ + "semantic_nodes": len(semantic_nodes), + "code_nodes": len(code_nodes), + "mappings": len(mappings), + "edges": len(context_edges), + "relation_edges": len(relation_edges), + "context_edges": len(context_edges), + }) + links.extend(relation_edges) + return { + "schema": "cmind.focused_graph.render.v1", + "summary": summary, + "nodes": nodes, + "links": links, + "relation_edges": relation_edges, + "semantic_nodes": semantic_nodes, + "code_nodes": code_nodes, + "mappings": mappings, + "edges": context_edges, + "hidden_counts": hidden_counts, + "hidden_context_nodes": hidden_context_nodes, + "warnings": warnings, + "hierarchy": hierarchy, + "default_focus": default_focus, + "node_aliases": node_aliases, + } + + +def _focused_graph_runtime() -> str: + return r""" +(function(){ + const section = document.currentScript.closest('[data-focused-graph]'); + if (!section) return; + const dataEl = section.querySelector('[data-focused-graph-json]'); + const svg = section.querySelector('[data-focused-graph-svg]'); + const stage = section.querySelector('.focused-graph-stage'); + const fallback = section.querySelector('[data-focused-graph-fallback]'); + const statusEl = section.querySelector('[data-focused-graph-status]'); + const detailEl = section.querySelector('[data-focused-graph-detail]'); + const fullscreenButton = section.querySelector('[data-action="fullscreen"]'); + if (!window.d3 || !dataEl || !svg) return; + if (fallback) fallback.hidden = true; + const data = JSON.parse(dataEl.textContent || '{}'); + const svgSelection = d3.select(svg); + let width = 960; + let height = 680; + let isFullscreen = false; + + function refreshGraphViewport() { + const svgBox = svg.getBoundingClientRect(); + const stageBox = stage ? stage.getBoundingClientRect() : {width: 0, height: 0}; + width = Math.max(320, Math.round(svgBox.width || stageBox.width || svg.clientWidth || 960)); + height = Math.max(360, Math.round(svgBox.height || stageBox.height || svg.clientHeight || 680)); + svgSelection.attr('viewBox', `0 0 ${width} ${height}`); + } + + function updateFullscreenButton() { + if (!fullscreenButton) return; + fullscreenButton.textContent = isFullscreen ? 'Restore embedded' : 'Fullscreen'; + fullscreenButton.setAttribute('aria-pressed', isFullscreen ? 'true' : 'false'); + } + + function toggleFullscreen() { + isFullscreen = !isFullscreen; + section.classList.toggle('focused-graph-fullscreen', isFullscreen); + document.body.classList.toggle('focused-graph-fullscreen-active', isFullscreen); + updateFullscreenButton(); + scheduleResize(); + } + + refreshGraphViewport(); + const defaultFocus = data.default_focus || {}; + const defaultShowEdges = defaultFocus.show_edges !== false; + const text = value => value === undefined || value === null ? '' : String(value); + const list = value => Array.isArray(value) ? value : []; + const nodePayloadById = {}; + list(data.nodes).forEach(node => { + if (!node || typeof node !== 'object') return; + const id = text(node.id); + const nodeId = text(node.node_id || node.dep_node_id); + if (id) nodePayloadById[id] = node; + if (nodeId && !nodePayloadById[nodeId]) nodePayloadById[nodeId] = node; + }); + const root = d3.hierarchy(data.hierarchy || {id:'focused-graph-root', name:'Focused graph', children:[]}, d => d.children); + const rootHierarchyId = text(root.data.id || 'focused-graph-root'); + const focusedNodeIds = new Set(list(defaultFocus.focused_tree_node_ids || defaultFocus.focused_node_ids || defaultFocus.node_link_ids).map(text).filter(Boolean)); + const focusedCodeLinkIds = new Set(list(defaultFocus.focused_code_link_ids).map(text).filter(Boolean)); + const expandedNodeIds = new Set(list(defaultFocus.default_expanded_node_ids || defaultFocus.expanded_node_ids).map(text).filter(Boolean)); + const focusedPathNodeIds = new Set(list(defaultFocus.focused_path_node_ids).map(text).filter(Boolean)); + const defaultExpandedIds = new Set([rootHierarchyId, ...expandedNodeIds]); + const isDefaultFocused = d => { + const data = d?.data || {}; + return [d?.id, data.id, data.link_id, data.node_id, data.dep_node_id] + .map(text) + .filter(Boolean) + .some(id => focusedNodeIds.has(id) || focusedCodeLinkIds.has(id)); + }; + let showEdges = defaultShowEdges; + let query = ''; + let selectedId = null; + const allNodeById = {}; + let nodeById = {}; + let currentNodes = []; + let currentRelationEdges = []; + + root.x0 = height / 2; + root.y0 = 0; + root.descendants().forEach(d => { + d.id = text(d.data.id || d.data.link_id || d.data.node_id || d.data.name); + if (!d.id) d.id = `node-${Math.random().toString(36).slice(2)}`; + d._allChildren = d.children || null; + allNodeById[d.id] = d; + }); + + function walkAll(d, visit) { + visit(d); + list(d._allChildren).forEach(child => walkAll(child, visit)); + } + + function initializeDefaultState() { + walkAll(root, d => { + if (!d._allChildren) return; + const keepOpen = d.depth === 0 || defaultExpandedIds.has(d.id) || focusedPathNodeIds.has(d.id); + if (keepOpen) { + d.children = d._allChildren; + d._children = null; + } else { + d._children = d._allChildren; + d.children = null; + } + }); + } + initializeDefaultState(); + + const relationEdges = list(data.relation_edges || data.links).filter(edge => text(edge.relation) !== 'contains'); + const treemap = d3.tree().nodeSize([28, 250]); + const graphOffsetX = 80; + const graphOffsetY = 96; + svgSelection.selectAll('*').remove(); + const graphLayer = svgSelection.append('g').attr('class', 'focused-graph-layer').attr('transform', `translate(${graphOffsetX},${graphOffsetY})`); + const relationLayer = graphLayer.append('g').attr('class', 'focused-graph-relation-links'); + const treeLinkLayer = graphLayer.append('g').attr('class', 'focused-graph-tree-links'); + const nodeLayer = graphLayer.append('g').attr('class', 'focused-graph-nodes'); + const zoom = d3.zoom().scaleExtent([0.25, 3.5]).on('zoom', event => graphLayer.attr('transform', event.transform)); + svgSelection.call(zoom).on('dblclick.zoom', null).on('click', event => { + if (event.target === svg) { + selectedId = null; + renderFocusedGraphDetail(null); + update(root); + } + }); + + function nodeDetailData(d) { + if (!d) return {}; + const data = d.data || {}; + const payload = nodePayloadById[text(d.id)] || nodePayloadById[text(data.node_id || data.dep_node_id)] || {}; + return {...payload, ...data}; + } + + function nodeLabel(d) { + const detail = nodeDetailData(d); + return text(detail.feature_name || detail.name || detail.node_id || detail.dep_node_id || detail.id || d.id || 'node'); + } + + function searchText(value) { + if (Array.isArray(value)) return value.map(searchText).join(' '); + if (value && typeof value === 'object') return Object.values(value).map(searchText).join(' '); + return text(value); + } + + function nodeSearchText(d) { + return `${nodeLabel(d)} ${searchText(nodeDetailData(d))}`.toLowerCase(); + } + + function nodeMatches(d) { + return query && nodeSearchText(d).includes(query); + } + + function isPresent(value) { + if (value === undefined || value === null || value === '') return false; + if (Array.isArray(value)) return value.length > 0; + return true; + } + + function escapeHtml(value) { + return text(value).replace(/[&<>"']/g, char => ({'&':'&','<':'<','>':'>','"':'"', "'":'''}[char])); + } + + function uniqueTexts(values) { + const seen = new Set(); + const result = []; + values.forEach(value => { + const item = text(value).trim(); + if (!item || seen.has(item)) return; + seen.add(item); + result.push(item); + }); + return result; + } + + function detailItems(value) { + if (!isPresent(value)) return []; + return Array.isArray(value) ? value : [value]; + } + + function lineRangeText(value) { + if (!isPresent(value)) return ''; + if (Array.isArray(value)) return value.map(text).filter(Boolean).join('-'); + if (value && typeof value === 'object') { + const start = value.start ?? value.start_line ?? value.line_start; + const end = value.end ?? value.end_line ?? value.line_end; + if (isPresent(start) && isPresent(end)) return `${start}-${end}`; + return searchText(value).trim(); + } + return text(value); + } + + function simpleValueHtml(value) { + if (!isPresent(value)) return ''; + if (Array.isArray(value)) return detailListHtml(value, simpleValueHtml); + if (value && typeof value === 'object') return `${escapeHtml(searchText(value).trim() || JSON.stringify(value))}`; + return escapeHtml(value); + } + + function detailListHtml(value, render) { + const rows = detailItems(value).map(render).filter(Boolean); + return rows.length ? `
      ${rows.map(row => `
    • ${row}
    • `).join('')}
    ` : ''; + } + + function changedFilesHtml(value) { + return detailListHtml(value, item => { + if (item && typeof item === 'object') { + const label = text(item.path || item.file || item.diff_anchor || item.href); + const href = text(item.href || (item.diff_anchor ? `#${item.diff_anchor}` : '')); + if (!label) return ''; + return href ? `${escapeHtml(label)}` : escapeHtml(label); + } + return escapeHtml(item); + }); + } + + function diffHtml(value, detail) { + if (value && typeof value === 'object') { + const label = text(value.path || value.href || 'View diff'); + const href = text(value.href || (value.diff_anchor ? `#${value.diff_anchor}` : '')); + return href ? `${escapeHtml(label)}` : escapeHtml(label); + } + if (detail.diff_anchor) return `${escapeHtml(detail.path || detail.diff_anchor)}`; + return ''; + } + + function canonicalMappedCodeRefs(value, detail) { + let refs = detailItems(value); + if (!refs.length) { + const paths = list(detail.mapped_code_paths); + const symbols = list(detail.mapped_code_symbols); + const count = Math.max(paths.length, symbols.length, detail.mapped_code_path ? 1 : 0, detail.mapped_code_symbol ? 1 : 0); + refs = Array.from({length: count}, (_, index) => ({ + path: index === 0 ? (detail.mapped_code_path || paths[index]) : paths[index], + symbol: index === 0 ? (detail.mapped_code_symbol || symbols[index]) : symbols[index], + })); + } + const seenRefs = new Set(); + const canonical = []; + refs.forEach(item => { + const ref = item && typeof item === 'object' + ? { + path: text(item.path || item.file || item.module), + symbol: text(item.symbol || item.name), + node_id: text(item.node_id || item.dep_node_id || item.link_id), + type: text(item.type || item.kind), + line_range: lineRangeText(item.line_range), + } + : {path: '', symbol: '', node_id: text(item), type: '', line_range: ''}; + const label = uniqueTexts([ref.path, ref.symbol]).join(' · ') || ref.node_id; + const key = uniqueTexts([ref.path, ref.symbol]).join('::') || ref.node_id || label; + if (!label || seenRefs.has(key)) return; + seenRefs.add(key); + canonical.push({label, meta: uniqueTexts([ref.type, ref.line_range]).join(' · ')}); + }); + return canonical; + } + + function mappedCodeHtml(value, detail) { + return detailListHtml(canonicalMappedCodeRefs(value, detail), ref => `${escapeHtml(ref.label)}${ref.meta ? `${escapeHtml(ref.meta)}` : ''}`); + } + + function addValueRow(rows, label, value) { + if (!isPresent(value)) return; + rows.push(`
    ${escapeHtml(label)}
    ${simpleValueHtml(value)}
    `); + } + + function addHtmlRow(rows, label, value) { + if (!value) return; + rows.push(`
    ${escapeHtml(label)}
    ${value}
    `); + } + + function isCodeContextDetail(detail) { + return uniqueTexts([detail.kind, detail.type, detail.node_type]) + .map(item => item.toLowerCase()) + .some(item => ['code', 'context', 'code_group', 'context_group'].includes(item)); + } + + function renderFocusedGraphDetail(d) { + if (!detailEl) return; + if (!d) { + detailEl.innerHTML = '

    Node details

    Select a node to inspect metadata.

    '; + return; + } + const detail = nodeDetailData(d); + const rows = []; + const typeText = uniqueTexts([detail.node_type || detail.type || detail.kind, detail.state, detail.mapping_status, detail.locate_status]).join(' · '); + const relationText = uniqueTexts([detail.relation, detail.direction]).join(' · '); + const sourceText = uniqueTexts([detail.source, detail.source_graph, detail.edge_source, detail.relation_source]).join(' · '); + addValueRow(rows, 'Node id', detail.node_id || detail.dep_node_id || detail.id); + if (!isCodeContextDetail(detail)) addValueRow(rows, 'Feature path', detail.breadcrumb_path || detail.feature_path); + addValueRow(rows, 'Path', detail.path || detail.module || detail.file); + addValueRow(rows, 'Symbol', detail.symbol); + addValueRow(rows, 'Type', typeText); + addValueRow(rows, 'Relation', relationText); + addValueRow(rows, 'Source', sourceText); + addValueRow(rows, 'Reason', detail.reason); + addValueRow(rows, 'Lines', lineRangeText(detail.line_range)); + addHtmlRow(rows, 'Mapped code', mappedCodeHtml(detail.mapped_code, detail)); + addHtmlRow(rows, 'Changed files', changedFilesHtml(detail.changed_files || detail.affected_files)); + addHtmlRow(rows, 'Diff', diffHtml(detail.diff, detail)); + const chips = uniqueTexts([detail.kind, detail.state, detail.mapping_status, detail.source]).map(item => `${escapeHtml(item)}`).join(''); + const body = rows.length ? `
    ${rows.join('')}
    ` : '

    No additional metadata for this node.

    '; + detailEl.innerHTML = `

    ${escapeHtml(nodeLabel(d))}

    ${chips ? `
    ${chips}
    ` : ''}${body}`; + } + + function diagonal(s, d) { + return `M${s.y},${s.x} C${(s.y + d.y) / 2},${s.x} ${(s.y + d.y) / 2},${d.x} ${d.y},${d.x}`; + } + + function openAncestors(d) { + let p = d.parent; + while (p) { + if (p._children) { p.children = p._children; p._children = null; } + p = p.parent; + } + } + + function applySearchOpen() { + if (!query) return; + walkAll(root, d => { if (nodeMatches(d)) openAncestors(d); }); + } + + function resetDefault() { + query = ''; + selectedId = null; + showEdges = defaultShowEdges; + initializeDefaultState(); + const search = section.querySelector('[data-action="search"]'); + if (search) search.value = ''; + const edges = section.querySelector('[data-action="edges"]'); + if (edges) edges.checked = showEdges; + renderFocusedGraphDetail(null); + svgSelection.transition().duration(150).call(zoom.transform, d3.zoomIdentity.translate(graphOffsetX, graphOffsetY)); + update(root); + } + + function cssToken(value) { + const token = text(value).toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, ''); + return token || 'unknown'; + } + + function edgeClass(edge) { + const kind = text(edge.kind || edge.source_graph).toLowerCase().includes('rpg') || text(edge.kind).toLowerCase() === 'semantic' ? 'edge-semantic' : 'edge-dependency'; + const relation = `relation-${cssToken(edge.relation || 'dependency')}`; + const source = `source-${cssToken(edge.source_graph || edge.edge_source || edge.source_meta || kind)}`; + return `${kind} ${relation} ${source}`; + } + + function visibleEndpoint(edge, side) { + for (const candidate of list(edge[`${side}_candidates`]).map(text)) { + let node = allNodeById[candidate] || nodeById[candidate]; + while (node) { + if (nodeById[node.id]) return nodeById[node.id]; + node = node.parent; + } + } + let node = allNodeById[text(edge[side])] || nodeById[text(edge[side])]; + while (node) { + if (nodeById[node.id]) return nodeById[node.id]; + node = node.parent; + } + return null; + } + + function relationPath(source, target) { + const sx = source.y + 8, sy = source.x; + const dx = target.y - 8, dy = target.x; + const midX = Math.max(sx, dx) + 56 + Math.abs(sy - dy) * 0.18; + return `M${sx},${sy} Q${midX},${(sy + dy) / 2} ${dx},${dy}`; + } + + function updateStatus() { + if (!statusEl) return; + const visible = showEdges ? currentRelationEdges.length : 0; + const total = relationEdges.length; + statusEl.textContent = `Visible relation edges: ${visible}/${total}`; + } + + function drawRelationEdges() { + currentRelationEdges = []; + if (showEdges) { + relationEdges.forEach(edge => { + const source = visibleEndpoint(edge, 'source'); + const target = visibleEndpoint(edge, 'target'); + if (!source || !target || source === target) return; + currentRelationEdges.push({...edge, _source: source, _target: target, _key: `${edge.id || edge.relation}-${source.id}-${target.id}`}); + }); + } + const rel = relationLayer.selectAll('path.focused-graph-link').data(currentRelationEdges, edge => edge._key); + const relEnter = rel.enter().append('path') + .attr('class', edge => `focused-graph-link ${edgeClass(edge)}`) + .attr('data-link-id', edge => text(edge.id || edge._key)) + .attr('d', edge => relationPath(edge._source, edge._target)); + relEnter.append('title'); + const relUpdate = relEnter.merge(rel) + .attr('class', edge => `focused-graph-link ${edgeClass(edge)}${selectedId && (edge._source.id === selectedId || edge._target.id === selectedId) ? ' active' : ''}${selectedId && edge._source.id !== selectedId && edge._target.id !== selectedId ? ' dimmed' : ''}`) + .attr('d', edge => relationPath(edge._source, edge._target)); + relUpdate.select('title').text(edge => `${edge.relation || 'dependency'}\n${edge.source_graph || edge.edge_source || edge.source_meta || edge.kind || ''}\n${edge.path || edge.reason || ''}`); + rel.exit().remove(); + updateStatus(); + } + + function update(source) { + applySearchOpen(); + const layout = treemap(root); + currentNodes = layout.descendants(); + const maxDepth = d3.max(currentNodes, d => d.depth) || 1; + const depthGap = Math.max(140, Math.min(250, (width - 260) / maxDepth)); + currentNodes.forEach(d => { d.y = d.depth * depthGap; }); + const minX = d3.min(currentNodes, d => d.x) || 0; + if (minX < 20) currentNodes.forEach(d => { d.x += 20 - minX; }); + nodeById = {}; + currentNodes.forEach(d => { nodeById[d.id] = d; }); + + const node = nodeLayer.selectAll('g.focused-graph-node').data(currentNodes, d => d.id); + const nodeEnter = node.enter().append('g') + .attr('class', 'focused-graph-node') + .attr('data-node-id', d => d.id) + .attr('tabindex', 0) + .attr('role', 'button') + .attr('transform', `translate(${source.y0 || 0},${source.x0 || 0})`) + .on('click', (event, d) => { + event.stopPropagation(); + selectedId = selectedId === d.id ? null : d.id; + renderFocusedGraphDetail(selectedId ? d : null); + update(d); + }) + .on('dblclick', (event, d) => { + event.stopPropagation(); + if (d.children) { d._children = d.children; d.children = null; } + else if (d._children) { d.children = d._children; d._children = null; } + update(d); + }); + nodeEnter.append('circle').attr('r', d => d._children ? 6 : (d.children ? 5 : 4)); + nodeEnter.append('text') + .attr('x', d => d.children || d._children ? 0 : 10) + .attr('y', d => d.children || d._children ? 14 : 0) + .attr('dy', d => d.children || d._children ? 8 : 4) + .attr('text-anchor', d => d.children || d._children ? 'middle' : 'start') + .attr('font-size', 12) + .attr('fill', '#e5e7eb') + .text(d => nodeLabel(d).length > 46 ? nodeLabel(d).slice(0, 44) + '…' : nodeLabel(d)); + nodeEnter.append('title').text(d => { + const detail = nodeDetailData(d); + return `${nodeLabel(d)}\n${detail.feature_path || ''}\n${detail.path || ''} ${detail.symbol || ''}`; + }); + + const nodeUpdate = nodeEnter.merge(node); + nodeUpdate.transition().duration(250).attr('transform', d => `translate(${d.y},${d.x})`); + nodeUpdate + .attr('class', d => `focused-graph-node${d.id === selectedId ? ' selected' : ''}${isDefaultFocused(d) ? ' focused' : ' non-focused'}${nodeMatches(d) ? ' search-match' : ''}${selectedId && d.id !== selectedId && !currentRelationEdges.some(edge => edge._source.id === d.id || edge._target.id === d.id) ? ' dimmed' : ''}`); + nodeUpdate.select('circle').attr('r', d => d._children ? 6 : (d.children ? 5 : 4)); + nodeUpdate.select('text') + .attr('x', d => d.children || d._children ? 0 : 10) + .attr('y', d => d.children || d._children ? 14 : 0) + .attr('dy', d => d.children || d._children ? 8 : 4) + .attr('text-anchor', d => d.children || d._children ? 'middle' : 'start'); + node.exit().transition().duration(180).attr('transform', `translate(${source.y || 0},${source.x || 0})`).remove(); + + const treeLinks = layout.links(); + const link = treeLinkLayer.selectAll('path.focused-graph-tree-link').data(treeLinks, d => d.target.id); + link.enter().insert('path', 'g') + .attr('class', 'focused-graph-tree-link') + .attr('d', () => diagonal({x: source.x0 || 0, y: source.y0 || 0}, {x: source.x0 || 0, y: source.y0 || 0})) + .merge(link).transition().duration(250) + .attr('d', d => diagonal(d.source, d.target)); + link.exit().transition().duration(180) + .attr('d', () => diagonal({x: source.x || 0, y: source.y || 0}, {x: source.x || 0, y: source.y || 0})) + .remove(); + + drawRelationEdges(); + renderFocusedGraphDetail(selectedId ? (nodeById[selectedId] || allNodeById[selectedId]) : null); + currentNodes.forEach(d => { d.x0 = d.x; d.y0 = d.y; }); + } + + let resizeFrame = null; + function scheduleResize() { + if (resizeFrame !== null) return; + resizeFrame = window.requestAnimationFrame(() => { + resizeFrame = null; + const previousWidth = width; + const previousHeight = height; + refreshGraphViewport(); + if (width !== previousWidth || height !== previousHeight) update(root); + }); + } + if (window.ResizeObserver) { + const resizeObserver = new ResizeObserver(scheduleResize); + resizeObserver.observe(svg); + if (stage) resizeObserver.observe(stage); + } else { + window.addEventListener('resize', scheduleResize); + } + + updateFullscreenButton(); + section.querySelector('[data-action="reset"]')?.addEventListener('click', resetDefault); + fullscreenButton?.addEventListener('click', toggleFullscreen); + section.querySelector('[data-action="edges"]')?.addEventListener('change', event => { showEdges = event.target.checked; update(root); }); + section.querySelector('[data-action="search"]')?.addEventListener('input', event => { query = text(event.target.value).toLowerCase(); update(root); }); + update(root); +})(); +""" + + +def _render_focused_graph(focused_view: dict[str, Any], file_anchors: Mapping[str, str]) -> str: + nodes_view = focused_view.get("nodes_view") if isinstance(focused_view.get("nodes_view"), Mapping) else {} + if not focused_view and not nodes_view: + return "" + graph_payload = _focused_graph_payload(focused_view, file_anchors) + summary = graph_payload.get("summary") if isinstance(graph_payload.get("summary"), Mapping) else {} + summary_html = _summary_badges(summary, [ + ("Semantic nodes", "semantic_nodes", len(_as_sequence(nodes_view.get("semantic_nodes")))), + ("Code nodes", "code_nodes", len(_as_sequence(nodes_view.get("code_nodes")))), + ("Mappings", "mappings", len(_as_sequence(nodes_view.get("mappings")))), + ("Edges", "edges", len(_as_sequence(nodes_view.get("edges")))), + ("Warnings", "warnings", len(_as_sequence(nodes_view.get("warnings")))), + ]) + hidden_counts = nodes_view.get("hidden_counts") if isinstance(nodes_view.get("hidden_counts"), Mapping) else focused_view.get("hidden_counts", {}) + hidden_context_nodes = [node for node in _as_sequence(nodes_view.get("hidden_context_nodes") or focused_view.get("hidden_context_nodes")) if isinstance(node, Mapping)] + hidden_html = _hidden_context_html(hidden_counts if isinstance(hidden_counts, Mapping) else {}, hidden_context_nodes) + warnings = [warning for warning in _as_sequence(nodes_view.get("warnings") or focused_view.get("warnings")) if isinstance(warning, Mapping)] + warnings_html = f"
    Warnings{_chain_warning_html(warnings)}
    " if warnings else "" + relation_edges_value = graph_payload.get("relation_edges") + if relation_edges_value is None: + relation_edges_value = graph_payload.get("edges") + relation_edge_total = len(_as_sequence(relation_edges_value)) + graph_json = _json_for_script(graph_payload) + d3_js = _inline_d3() + fallback_hidden = " hidden" if d3_js else "" + d3_missing_note = "" if d3_js else '

    Local D3 asset missing; showing the static fallback.

    ' + scripts = "" + if d3_js: + scripts = f"" + inspector_payload = {"focused_graph": graph_payload, "nodes_view": nodes_view} + inspector = json.dumps(inspector_payload, indent=2, ensure_ascii=False, default=_json_default) + controls = ( + '
    ' + '' + '' + '' + '' + f'Visible relation edges: 0/{relation_edge_total}' + '
    ' + ) + legend = ( + '
    ' + 'Feature tree node' + 'Tree link' + 'RPG semantic edge' + 'dep_graph dependency edge' + 'invokes' + 'imports' + 'inherits' + 'references' + '
    ' + ) + return ( + '

    Focused graph

    ' + f"{summary_html}" + f"" + '
    ' + f"{controls}{legend}" + '' + '' + f'
    Static focused graph fallback is available when D3 cannot run.{d3_missing_note}
    ' + '
    ' + f"{warnings_html}{hidden_html}" + f"
    Inspector JSON
    {_h(inspector)}
    " + f"{scripts}" + '
    ' + ) + + +def _legacy_render_focused_nodes_map(focused_view: dict[str, Any], file_anchors: Mapping[str, str]) -> str: + nodes_view = focused_view.get("nodes_view") if isinstance(focused_view.get("nodes_view"), Mapping) else {} + if not nodes_view: + return "" + summary = nodes_view.get("summary") if isinstance(nodes_view.get("summary"), Mapping) else {} + summary_html = _summary_badges(summary, [ + ("Semantic nodes", "semantic_nodes", len(_as_sequence(nodes_view.get("semantic_nodes")))), + ("Code nodes", "code_nodes", len(_as_sequence(nodes_view.get("code_nodes")))), + ("Mappings", "mappings", len(_as_sequence(nodes_view.get("mappings")))), + ("Edges", "edges", len(_as_sequence(nodes_view.get("edges")))), + ("Warnings", "warnings", len(_as_sequence(nodes_view.get("warnings")))), + ]) + semantic_cards = [ + _semantic_card(node, file_anchors) + for node in _as_sequence(nodes_view.get("semantic_nodes")) + if isinstance(node, Mapping) + ] + code_cards = [ + _code_card(node, file_anchors) + for node in _as_sequence(nodes_view.get("code_nodes")) + if isinstance(node, Mapping) + ] + mapping_cards = [ + _mapping_card(mapping) + for mapping in _as_sequence(nodes_view.get("mappings")) + if isinstance(mapping, Mapping) + ] + hidden_html = _hidden_context_html( + nodes_view.get("hidden_counts") if isinstance(nodes_view.get("hidden_counts"), Mapping) else {}, + _as_sequence(nodes_view.get("hidden_context_nodes")), + ) + warnings = [warning for warning in _as_sequence(nodes_view.get("warnings")) if isinstance(warning, Mapping)] + warnings_html = f"

    Warnings

    {_chain_warning_html(warnings)}" if warnings else "" + body = summary_html + if semantic_cards or code_cards or mapping_cards: + body += '
    ' + "".join(semantic_cards + code_cards + mapping_cards) + "
    " + else: + body += '

    No focused nodes map rows recorded.

    ' + body += hidden_html + warnings_html + _focused_graph_metadata(focused_view) + return f"

    Focused nodes map

    {body}
    " + + +def _render_feature_chain_rows(focused_view: dict[str, Any], file_anchors: Mapping[str, str]) -> str: + rpg_nodes = [node for node in _as_sequence(focused_view.get("primary_rpg_nodes")) if isinstance(node, Mapping)] + code_nodes = [node for node in _as_sequence(focused_view.get("primary_code_nodes")) if isinstance(node, Mapping)] + mappings = [mapping for mapping in _as_sequence(focused_view.get("mappings")) if isinstance(mapping, Mapping)] + edges = [edge for edge in _as_sequence(focused_view.get("edges")) if isinstance(edge, Mapping)] + warnings = [warning for warning in _as_sequence(focused_view.get("warnings")) if isinstance(warning, Mapping)] + if not rpg_nodes and not mappings and not code_nodes: + return "

    No semantic-code impact chain rows recorded.

    " + + rpg_by_id = {str(node.get("node_id")): node for node in rpg_nodes if node.get("node_id") not in (None, "")} + code_by_id = { + str(node.get("node_id") or node.get("dep_node_id")): node + for node in code_nodes + if (node.get("node_id") or node.get("dep_node_id")) not in (None, "") + } + mappings_by_rpg: dict[str, list[Mapping[str, Any]]] = {} + mapped_code_ids_by_rpg: dict[str, set[str]] = {} + for mapping in mappings: + rpg_id = mapping.get("rpg_node_id") or mapping.get("node_id") or "" + rpg_text = str(rpg_id) + mappings_by_rpg.setdefault(rpg_text, []).append(mapping) + code_id = mapping.get("code_node_id") or mapping.get("dep_node_id") + if code_id not in (None, ""): + mapped_code_ids_by_rpg.setdefault(rpg_text, set()).add(str(code_id)) + if rpg_text and rpg_text not in rpg_by_id: + rpg_by_id[rpg_text] = {"node_id": rpg_text, "mapping_status": mapping.get("status", "")} + edge_rows_by_rpg: dict[str, list[Mapping[str, Any]]] = {str(node_id): [] for node_id in rpg_by_id} + for edge in edges: + rpg_id = edge.get("rpg_node_id") + matched = False + if rpg_id not in (None, "") and str(rpg_id) in edge_rows_by_rpg: + edge_rows_by_rpg[str(rpg_id)].append(edge) + matched = True + source = str(edge.get("source_node_id") or "") + target = str(edge.get("target_node_id") or "") + for node_id, code_ids in mapped_code_ids_by_rpg.items(): + if source in code_ids or target in code_ids: + edge_rows_by_rpg.setdefault(node_id, []).append(edge) + matched = True + if not matched and len(edge_rows_by_rpg) == 1: + only_id = next(iter(edge_rows_by_rpg)) + edge_rows_by_rpg[only_id].append(edge) + + warnings_by_rpg: dict[str, list[Mapping[str, Any]]] = {str(node_id): [] for node_id in rpg_by_id} + global_warnings = [] + for warning in warnings: + node_id = warning.get("node_id") or warning.get("rpg_node_id") + if node_id not in (None, "") and str(node_id) in warnings_by_rpg: + warnings_by_rpg[str(node_id)].append(warning) + else: + global_warnings.append(warning) + + rows = [] + for node_id, rpg_node in rpg_by_id.items(): + node_mappings = mappings_by_rpg.get(node_id) or [] + mapping_items = [] + changed_files: list[Any] = list(_as_sequence(rpg_node.get("changed_files"))) + for mapping in node_mappings: + code_id = mapping.get("code_node_id") or mapping.get("dep_node_id") + code_node = code_by_id.get(str(code_id)) if code_id not in (None, "") else None + changed_files.extend(_mapping_changed_files(mapping, rpg_node)) + status = mapping.get("status") or rpg_node.get("mapping_status") or "recorded" + source = mapping.get("source", "") + reason = mapping.get("reason") or rpg_node.get("reason") or "" + path = mapping.get("path") or (code_node or {}).get("path") or "" + mapping_items.append( + "
  • " + f"{_h(status)} {_focused_node_cell(code_id, code_node)}" + f"
    {_h(source)} {_h(path)} {_h(reason)}
    " + "
  • " + ) + if not mapping_items: + mapping_items.append(f"
  • {_h(rpg_node.get('mapping_status') or rpg_node.get('status') or 'recorded')} No mapped code node.
  • ") + mapping_html = '
      ' + "".join(mapping_items) + "
    " + node_hidden = rpg_node.get("hidden_counts") if isinstance(rpg_node.get("hidden_counts"), Mapping) else {} + hidden_html = _hidden_context_html(node_hidden) + rows.append( + "" + f"{_focused_node_cell(node_id, rpg_node)}
    {_h(rpg_node.get('reason', ''))}
    " + f"{mapping_html}" + f"{_changed_file_links(changed_files or _as_sequence(rpg_node.get('affected_files')), file_anchors)}" + f"{_chain_edge_html(edge_rows_by_rpg.get(node_id, []))}{hidden_html}" + f"{_chain_warning_html(warnings_by_rpg.get(node_id, []))}" + "" + ) + + orphan_code_nodes = [ + node for code_id, node in code_by_id.items() + if not any(code_id in ids for ids in mapped_code_ids_by_rpg.values()) + ] + if orphan_code_nodes: + orphan_items = "".join( + f"
  • {_focused_node_cell(node.get('node_id') or node.get('dep_node_id'), node)} {_h(node.get('source', ''))}
  • " + for node in orphan_code_nodes + ) + rows.append( + "" + "Changed code without selected feature" + f"
      {orphan_items}
    " + "No changed files mapped." + "No visible neighborhood edges." + "No warnings." + "" + ) + + table = ( + "" + "" + + "".join(rows) + + "
    Feature groupSemantic → code evidenceChanged filesNeighborhoodWarnings
    " + ) + if global_warnings: + table += f"

    Global warnings

    {_chain_warning_html(global_warnings)}" + return table + + +def _render_legacy_chain_rows( + retrievals: list[dict[str, Any]], + rpg_nodes: list[dict[str, Any]], + dep_nodes: list[dict[str, Any]], +) -> str: + blocks = [] + if retrievals: + blocks.append(_render_retrievals(retrievals, title="Retrieval audit trail", as_section=False)) + if rpg_nodes or dep_nodes: + rows = [] + dep_by_feature: dict[str, list[dict[str, Any]]] = {} + for dep_node in dep_nodes: + source_feature = str(dep_node.get("source_feature") or "") + dep_by_feature.setdefault(source_feature, []).append(dep_node) + for node in rpg_nodes or [{"node_id": ""}]: + node_id = str(node.get("node_id") or "") + mapped = dep_by_feature.get(node_id) or ([] if node_id else dep_nodes) + mapping_html = "No mapped code nodes recorded." + if mapped: + items = [] + for dep_node in mapped: + dep_id = dep_node.get("dep_node_id") or dep_node.get("node_id") + items.append( + "
  • " + f"{_h(dep_id)} {_h(dep_node.get('path', ''))}" + f" {_h(dep_node.get('relation') or dep_node.get('change') or dep_node.get('status', ''))}" + "
  • " + ) + mapping_html = '
      ' + "".join(items) + "
    " + rows.append( + "" + f"{_focused_node_cell(node_id, node)}" + f"{mapping_html}" + "No changed files mapped." + "No visible neighborhood edges." + "No warnings." + "" + ) + blocks.append( + "" + "" + + "".join(rows) + + "
    Feature groupSemantic → code evidenceChanged filesNeighborhoodWarnings
    " + ) + return "".join(blocks) + + +def _legacy_render_semantic_code_impact_chain( + retrievals: list[dict[str, Any]], + rpg_nodes: list[dict[str, Any]], + dep_nodes: list[dict[str, Any]], + focused_view: dict[str, Any], + file_anchors: Mapping[str, str], +) -> str: + if not focused_view and not retrievals and not rpg_nodes and not dep_nodes: + return "" + if focused_view: + summary = focused_view.get("summary") if isinstance(focused_view.get("summary"), Mapping) else {} + summary_html = _summary_badges(summary, [ + ("Selected feature groups", "selected_feature_groups", len(_as_sequence(focused_view.get("primary_rpg_nodes")))), + ("Primary code nodes", "primary_code_nodes", len(_as_sequence(focused_view.get("primary_code_nodes")))), + ("Mapped relations", "mapped_code_relations", len(_as_sequence(focused_view.get("mappings")))), + ("Missing mappings", "missing_mappings", 0), + ("Edges shown", "edges", len(_as_sequence(focused_view.get("edges")))), + ("Warnings", "warnings", len(_as_sequence(focused_view.get("warnings")))), + ]) + hidden_counts = focused_view.get("hidden_counts") if isinstance(focused_view.get("hidden_counts"), Mapping) else {} + hidden_html = _hidden_context_html(hidden_counts) + inspector_html = "" if isinstance(focused_view.get("nodes_view"), Mapping) else _focused_graph_metadata(focused_view) + body = f"{summary_html}{_render_feature_chain_rows(focused_view, file_anchors)}{hidden_html}{inspector_html}" + else: + body = _render_legacy_chain_rows(retrievals, rpg_nodes, dep_nodes) + return f"

    semantic-code impact chain

    {body}
    " + + +def _focused_graph_metadata(focused_view: dict[str, Any]) -> str: + data = json.dumps(_focused_inspector_payload(focused_view), indent=2, ensure_ascii=False, default=_json_default) + return f"
    Inspector JSON
    {_h(data)}
    " + + +def _focused_node_cell(node_id: Any, node: Mapping[str, Any] | None) -> str: + if node_id in (None, "") and not node: + return "missing" + node = node or {} + parts = [] + if node_id not in (None, ""): + parts.append(f"{_h(node_id)}") + if node.get("name"): + parts.append(f"
    {_h(node.get('name'))}
    ") + if node.get("path"): + parts.append(f"
    {_h(node.get('path'))}
    ") + return "".join(parts) or "missing" + + +def _render_artifacts(artifacts: list[dict[str, Any]]) -> str: + if not artifacts: + body = "

    No artifact links recorded.

    " + else: + rows = [] + for artifact in artifacts: + path = artifact.get("path") + href = _artifact_href(path) + rows.append( + "" + f"{_h(artifact.get('label', 'artifact'))}" + f"{_h(path or '')}" + f"{_h(_artifact_status(path, artifact.get('status')))}" + "" + ) + body = "" + "".join(rows) + "
    ArtifactPathStatus
    " + return f"

    Artifact links

    {body}
    " + + +def _compact_artifact_pointers(value: Any) -> list[dict[str, Any]]: + pointers = [] + for item in _as_sequence(value): + if not isinstance(item, Mapping): + continue + row: dict[str, Any] = {} + for key in ("label", "path", "status"): + if item.get(key) not in (None, ""): + row[key] = item.get(key) + if row: + pointers.append(row) + return pointers + + +def _compact_change_summary(value: Any) -> list[dict[str, Any]]: + rows = [] + for item in _as_sequence(value): + if not isinstance(item, Mapping): + continue + diff = item.get("diff") or "" + row: dict[str, Any] = {"file": item.get("file") or item.get("path") or ""} + if item.get("change_type") not in (None, ""): + row["change_type"] = item.get("change_type") + row["has_diff"] = bool(diff) + if diff: + row["diff_lines"] = len(str(diff).splitlines()) + rows.append(row) + return rows + + +def _compact_payload(value: Any, *, depth: int = 0) -> Any: + if depth > 4: + return "..." + if isinstance(value, Mapping): + compacted: dict[str, Any] = {} + for key, item in value.items(): + key_text = str(key) + if key_text in {"code_deltas", "focused_view", "nodes_view", "focused_impact", "focused_graph", "evidence"}: + continue + if key_text == "diff": + compacted["has_diff"] = bool(item) + if item: + compacted["diff_lines"] = len(str(item).splitlines()) + continue + if key_text == "artifacts": + artifact_pointers = _compact_artifact_pointers(item) + if artifact_pointers: + compacted["artifact_paths"] = artifact_pointers + continue + compact_item = _compact_payload(item, depth=depth + 1) + if compact_item in (None, "", [], {}): + continue + compacted[key_text] = compact_item + return compacted + if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + rows = [] + for item in value: + compact_item = _compact_payload(item, depth=depth + 1) + if compact_item not in (None, "", [], {}): + rows.append(compact_item) + return rows + return value + + +def _compact_evidence(evidence: Mapping[str, Any]) -> dict[str, Any]: + compacted: dict[str, Any] = {} + artifacts = _compact_artifact_pointers(evidence.get("artifacts")) + if artifacts: + compacted["artifact_paths"] = artifacts + for key in ("command", "title", "status", "timestamp", "summary", "steps", "retrievals", "rpg_deltas", "dep_graph_deltas", "verification", "user_decisions"): + value = evidence.get(key) + if value in (None, "", [], {}): + continue + compacted[key] = _compact_payload(value) + change_summary = _compact_change_summary(evidence.get("code_deltas")) + if change_summary: + compacted["changed_files"] = change_summary + nested = evidence.get("evidence") + if isinstance(nested, Mapping): + nested_artifacts = _compact_artifact_pointers(nested.get("artifact_paths") or nested.get("artifacts")) + if nested_artifacts and not artifacts: + compacted["artifact_paths"] = nested_artifacts + audit_source = nested.get("audit_summary") if isinstance(nested.get("audit_summary"), Mapping) else { + key: value for key, value in nested.items() + if key not in {"artifact_paths", "artifacts"} + } + audit = _compact_payload(audit_source) + if audit: + compacted["audit_summary"] = audit + return compacted + + +def _render_evidence(evidence: Mapping[str, Any]) -> str: + data = json.dumps(_compact_evidence(evidence), indent=2, ensure_ascii=False, default=_json_default) + return f"
    Evidence JSON
    {_h(data)}
    " + + +def _artifact_href(path: Any) -> str: + if path is None: + return "#" + try: + return Path(str(path)).expanduser().resolve().as_uri() + except Exception: + return "file://" + quote(str(path), safe="/._-~") + + +def _json_default(value: Any) -> Any: + if isinstance(value, Path): + return str(value) + if hasattr(value, "to_dict"): + return value.to_dict() + if hasattr(value, "__dict__"): + return value.__dict__ + return str(value) + + +def _h(value: Any) -> str: + if value is None: + return "" + if isinstance(value, (dict, list, tuple)): + value = json.dumps(value, ensure_ascii=False, default=_json_default) + return escape(str(value), quote=False) + + +def _h_attr(value: Any) -> str: + if value is None: + return "" + return escape(str(value), quote=True) diff --git a/CoderMind/scripts/common/session_manager.py b/CoderMind/scripts/common/session_manager.py index df8fd5a..daa0a23 100644 --- a/CoderMind/scripts/common/session_manager.py +++ b/CoderMind/scripts/common/session_manager.py @@ -225,7 +225,7 @@ class NullSessionManager(SessionManager): """ def before(self, ctx: TraceContext, prompt: str) -> None: - pass + return None def after(self, purpose: str) -> Optional[Path]: return None diff --git a/CoderMind/scripts/common/utils.py b/CoderMind/scripts/common/utils.py index 1dd1874..a4f3809 100644 --- a/CoderMind/scripts/common/utils.py +++ b/CoderMind/scripts/common/utils.py @@ -857,9 +857,6 @@ def get_skeleton( class _CompressTransformer(cst.CSTTransformer): """Replace function bodies with ``...`` while preserving structure.""" - def __init__(self): - pass - def _is_import_stmt(self, stmt: cst.CSTNode) -> bool: if not m.matches(stmt, m.SimpleStatementLine()): return False diff --git a/CoderMind/scripts/decoder_lang/python_backend.py b/CoderMind/scripts/decoder_lang/python_backend.py index 06384dd..17717cb 100644 --- a/CoderMind/scripts/decoder_lang/python_backend.py +++ b/CoderMind/scripts/decoder_lang/python_backend.py @@ -14,6 +14,7 @@ import ast import keyword import logging +import sys from pathlib import Path from typing import Any @@ -22,7 +23,7 @@ from .prompt_hints import PromptHints from .project_tasks import ProjectTaskContext, ProjectTaskTemplates from .unit_kind import classify_unit_kind -from .test_result import EnvHandle, TestRunResult +from .test_result import EnvHandle, TestFailure, TestRunResult, ran_no_tests logger = logging.getLogger(__name__) @@ -419,23 +420,34 @@ def _source_for_node(source: str, node: ast.AST) -> str: return "" # ------------------------------------------------------------------ - # 3. Build / test environment — not wired into the decoder yet + # 3. Build / test environment # ------------------------------------------------------------------ def detect_env(self, repo_root: Path) -> EnvHandle | None: - """Return an existing Python test environment when supported.""" - raise NotImplementedError( - "PythonBackend.detect_env is not wired into the decoder; " - "callers should keep using code_gen.test_runner.get_dev_python " - "for now.", + """Return the existing managed Python test environment, if present.""" + from code_gen.test_runner import get_dev_python, get_dev_venv_path + + root = repo_root.resolve() + py = get_dev_python(root) + if py is None: + return None + return EnvHandle( + project_root=root, + runtime_executable=py, + extra={"venv": str(get_dev_venv_path(root))}, ) def ensure_env(self, repo_root: Path) -> EnvHandle: - """Always available on a host that's already running Python - (the decoder itself), so this never raises - :class:`ToolchainUnavailable` once implemented.""" - raise NotImplementedError( - "PythonBackend.ensure_env is not wired into the decoder.", + """Create or reuse the managed Python test environment.""" + from code_gen.test_runner import ensure_dev_venv, get_dev_python, get_dev_venv_path + + root = repo_root.resolve() + ensure_dev_venv(root) + py = get_dev_python(root) or sys.executable + return EnvHandle( + project_root=root, + runtime_executable=py, + extra={"venv": str(get_dev_venv_path(root))}, ) def test_command( @@ -443,33 +455,77 @@ def test_command( env: EnvHandle, selectors: list[str] | None = None, ) -> list[str]: - """Return the command used to run Python tests when supported.""" - raise NotImplementedError( - "PythonBackend.test_command is not wired into the decoder; " - "callers should keep using code_gen.batch_prompts." - "build_batch_pytest_cmd for now.", - ) + """Return the pytest command for a Python environment.""" + python = env.runtime_executable or sys.executable + cmd = [python, "-m", "pytest"] + if selectors: + cmd.extend(["-k", " or ".join(selectors)]) + return cmd def install_deps_command( self, env: EnvHandle, deps: list[str], ) -> list[str] | None: - """Return a dependency-install command when supported.""" - raise NotImplementedError( - "PythonBackend.install_deps_command is not wired into the decoder.", - ) + """Return a pip/uv install command for missing Python deps.""" + if not deps: + return None + from code_gen.test_runner import _build_pip_cmd + + return _build_pip_cmd(deps, env.project_root) # ------------------------------------------------------------------ - # 4. Test-output parsing — not wired into the decoder yet + # 4. Test-output parsing # ------------------------------------------------------------------ def parse_test_output(self, raw: str, exit_code: int) -> TestRunResult: - """Parse native Python test output when backend-driven tests run.""" - raise NotImplementedError( - "PythonBackend.parse_test_output is not wired into the decoder; " - "callers should keep using code_gen.test_output_parser." - "analyze_test_output for now.", + """Parse pytest output into the backend-neutral result shape.""" + from code_gen.test_output_parser import analyze_test_output + + analysis = analyze_test_output(raw) + observed = analysis.passed + analysis.failed + analysis.errors + failures: list[TestFailure] = [] + if (analysis.failed or analysis.errors) and analysis.failure_lines: + test_id = analysis.failing_test_files[0] if analysis.failing_test_files else "pytest" + failures.append(TestFailure( + test_id=test_id, + short_message=analysis.failure_lines.splitlines()[0], + long_message=analysis.failure_lines, + file_path=analysis.failing_test_files[0] if analysis.failing_test_files else None, + )) + + if ran_no_tests(exit_code, raw, observed_tests=observed or None): + status = "errored" + error_count = analysis.errors or 1 + elif exit_code == 0: + status = "passed" + error_count = analysis.errors + else: + status = "failed" + error_count = analysis.errors + + return TestRunResult( + status=status, + exit_code=exit_code, + passed_count=analysis.passed, + failed_count=analysis.failed, + error_count=error_count, + skipped_count=analysis.skipped, + duration_sec=analysis.duration, + failures=failures, + raw_output=raw, + extra={ + "tool": "pytest", + "failure_type": analysis.failure_type, + "env_sub_type": analysis.env_sub_type, + "env_fix_target": analysis.env_fix_target, + "env_instruction": analysis.env_instruction, + "env_details": analysis.env_details, + "missing_names": analysis.missing_names, + "missing_modules": analysis.missing_modules, + "failing_test_files": analysis.failing_test_files, + "has_tests_run": analysis.has_tests_run, + }, ) # ------------------------------------------------------------------ diff --git a/CoderMind/scripts/decoder_lang/tests/test_python_backend.py b/CoderMind/scripts/decoder_lang/tests/test_python_backend.py index 8e57001..d81bb73 100644 --- a/CoderMind/scripts/decoder_lang/tests/test_python_backend.py +++ b/CoderMind/scripts/decoder_lang/tests/test_python_backend.py @@ -1,15 +1,16 @@ """Tests for the decoder backend registry and Python backend contract. These tests focus on invariants relied on by code paths that already -route through :mod:`decoder_lang`. Unsupported methods are asserted to -raise ``NotImplementedError`` so accidental partial implementations are -visible. +route through :mod:`decoder_lang`, including Python-specific environment +and pytest result handling. """ from __future__ import annotations import sys +import types import unittest from pathlib import Path +from unittest.mock import Mock, patch # Make ``scripts/`` importable when these tests are run directly. _SCRIPTS_DIR = Path(__file__).resolve().parents[2] @@ -200,35 +201,133 @@ def test_syntax_check(self) -> None: self.assertIn("SyntaxError", err or "") -class StubbedMethodsTests(unittest.TestCase): - """Unsupported methods must raise instead of returning bad data.""" +class BackendRuntimeTests(unittest.TestCase): + """Python backend build/test environment and pytest parsing.""" def setUp(self) -> None: self.backend = get_backend("python") - def test_detect_env_stub(self) -> None: - with self.assertRaises(NotImplementedError): - self.backend.detect_env(Path(".")) + def _fake_test_runner( + self, + *, + python: str | None = "/repo/.cmind-dev-venv/bin/python", + venv: Path = Path("/repo/.cmind-dev-venv"), + pip_cmd: list[str] | None = None, + ) -> types.ModuleType: + runner = types.ModuleType("code_gen.test_runner") + runner.get_dev_python = Mock(return_value=python) + runner.get_dev_venv_path = Mock(return_value=venv) + runner.ensure_dev_venv = Mock(return_value=(python is None, venv)) + runner._build_pip_cmd = Mock(return_value=pip_cmd or ["pip", "install"]) + return runner + + def test_detect_env_returns_none_without_existing_dev_python(self) -> None: + repo_root = Path("/repo") + runner = self._fake_test_runner(python=None) + + with patch.dict(sys.modules, {"code_gen.test_runner": runner}): + self.assertIsNone(self.backend.detect_env(repo_root)) + + runner.get_dev_python.assert_called_once_with(repo_root.resolve()) + runner.get_dev_venv_path.assert_not_called() + + def test_detect_env_returns_existing_dev_python_handle(self) -> None: + repo_root = Path("/repo") + runner = self._fake_test_runner() + + with patch.dict(sys.modules, {"code_gen.test_runner": runner}): + env = self.backend.detect_env(repo_root) + + self.assertIsNotNone(env) + assert env is not None + self.assertEqual(env.project_root, repo_root.resolve()) + self.assertEqual(env.runtime_executable, "/repo/.cmind-dev-venv/bin/python") + self.assertEqual(env.extra["venv"], "/repo/.cmind-dev-venv") + + def test_ensure_env_creates_or_reuses_dev_python_handle(self) -> None: + repo_root = Path("/repo") + runner = self._fake_test_runner() + + with patch.dict(sys.modules, {"code_gen.test_runner": runner}): + env = self.backend.ensure_env(repo_root) + + runner.ensure_dev_venv.assert_called_once_with(repo_root.resolve()) + runner.get_dev_python.assert_called_once_with(repo_root.resolve()) + self.assertEqual(env.project_root, repo_root.resolve()) + self.assertEqual(env.runtime_executable, "/repo/.cmind-dev-venv/bin/python") + self.assertEqual(env.extra["venv"], "/repo/.cmind-dev-venv") + + def test_test_command_uses_runtime_and_selectors(self) -> None: + from decoder_lang.test_result import EnvHandle - def test_ensure_env_stub(self) -> None: - with self.assertRaises(NotImplementedError): - self.backend.ensure_env(Path(".")) + env = EnvHandle(project_root=Path("/repo"), runtime_executable="/venv/bin/python") + self.assertEqual( + self.backend.test_command(env, selectors=["AlphaTests", "test_beta"]), + ["/venv/bin/python", "-m", "pytest", "-k", "AlphaTests or test_beta"], + ) - def test_test_command_stub(self) -> None: + def test_test_command_falls_back_to_current_python(self) -> None: from decoder_lang.test_result import EnvHandle - with self.assertRaises(NotImplementedError): - self.backend.test_command(EnvHandle(project_root=Path("."))) - def test_install_deps_command_stub(self) -> None: + env = EnvHandle(project_root=Path("/repo")) + self.assertEqual(self.backend.test_command(env), [sys.executable, "-m", "pytest"]) + + def test_install_deps_command_returns_none_for_no_deps(self) -> None: from decoder_lang.test_result import EnvHandle - with self.assertRaises(NotImplementedError): - self.backend.install_deps_command( - EnvHandle(project_root=Path(".")), deps=["x"], - ) - - def test_parse_test_output_stub(self) -> None: - with self.assertRaises(NotImplementedError): - self.backend.parse_test_output("foo", 0) + + env = EnvHandle(project_root=Path("/repo")) + self.assertIsNone(self.backend.install_deps_command(env, deps=[])) + + def test_install_deps_command_delegates_to_test_runner(self) -> None: + from decoder_lang.test_result import EnvHandle + + repo_root = Path("/repo") + runner = self._fake_test_runner(pip_cmd=["uv", "pip", "install", "requests"]) + env = EnvHandle(project_root=repo_root) + + with patch.dict(sys.modules, {"code_gen.test_runner": runner}): + cmd = self.backend.install_deps_command(env, deps=["requests"]) + + self.assertEqual(cmd, ["uv", "pip", "install", "requests"]) + runner._build_pip_cmd.assert_called_once_with(["requests"], repo_root) + + def test_parse_test_output_reports_passing_pytest_output(self) -> None: + output = "============================== 3 passed in 0.12s ==============================" + + result = self.backend.parse_test_output(output, 0) + + self.assertEqual(result.status, "passed") + self.assertEqual(result.exit_code, 0) + self.assertEqual(result.passed_count, 3) + self.assertEqual(result.failed_count, 0) + self.assertEqual(result.error_count, 0) + self.assertEqual(result.duration_sec, 0.12) + self.assertTrue(result.extra["has_tests_run"]) + + def test_parse_test_output_reports_failing_pytest_output(self) -> None: + output = ( + "FAILED tests/test_sample.py::test_bad - AssertionError: no\n" + "E AssertionError: no\n" + "========================= 1 failed, 2 passed in 0.50s =========================" + ) + + result = self.backend.parse_test_output(output, 1) + + self.assertEqual(result.status, "failed") + self.assertEqual(result.passed_count, 2) + self.assertEqual(result.failed_count, 1) + self.assertEqual(result.duration_sec, 0.50) + self.assertEqual(result.failures[0].test_id, "tests/test_sample.py") + self.assertEqual(result.failures[0].file_path, "tests/test_sample.py") + self.assertIn("AssertionError", result.failures[0].long_message) + + def test_parse_test_output_flags_empty_success_as_errored(self) -> None: + result = self.backend.parse_test_output("", 0) + + self.assertEqual(result.status, "errored") + self.assertEqual(result.error_count, 1) + self.assertEqual(result.passed_count, 0) + self.assertFalse(result.extra["has_tests_run"]) class PromptHintsTests(unittest.TestCase): diff --git a/CoderMind/scripts/plan.py b/CoderMind/scripts/plan.py index 90c2cd5..a6ad4aa 100644 --- a/CoderMind/scripts/plan.py +++ b/CoderMind/scripts/plan.py @@ -58,7 +58,20 @@ # Sub-scripts live in the same directory as this file (bundled under # cmind_cli/core_pack/scripts/ in the installed wheel). _SCRIPTS_DIR = Path(__file__).resolve().parent - +if str(_SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(_SCRIPTS_DIR)) + +from common.paths import ( + BASE_CLASSES_FILE, + DATA_FLOW_FILE, + DATA_FLOW_VIZ_FILE, + INTERFACES_FILE, + SKELETON_FILE, + SKELETON_SUMMARY_FILE, + TASKS_FILE, +) +from common.run_events import ArtifactEvent, CommandRun, StepEvent, VerificationEvent +from common.run_report import write_command_report # --------------------------------------------------------------------------- # Stage table — single source of truth for the pipeline. @@ -278,8 +291,9 @@ def decide(states: list[StageState], force: bool) -> None: state.reason = "up-to-date" else: state.will_run = True - if state.type == "warning": - state.reason = "warning: cross-stage contract violation; rebuild stage and downstream" + # Preserve useful messages for warnings (don't overwrite state.message) + if state.message: + state.reason = state.message else: state.reason = f"type={state.type}" cascade = True @@ -292,6 +306,96 @@ def decide(states: list[StageState], force: bool) -> None: _GLYPH = {"update": "✓", "init": "·", "warning": "!", "error": "✗"} +def _stage_rows(states: list[StageState]) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + for state in states: + rows.append({ + "name": state.stage.name, + "status": "run" if state.will_run else "skip", + "type": state.type, + "done": state.done, + "reason": state.reason or state.message, + "message": state.message, + }) + return rows + + +def _plan_artifacts() -> list[dict[str, Any]]: + paths = { + "skeleton": SKELETON_FILE, + "skeleton_summary": SKELETON_SUMMARY_FILE, + "data_flow": DATA_FLOW_FILE, + "data_flow_viz": DATA_FLOW_VIZ_FILE, + "base_classes": BASE_CLASSES_FILE, + "interfaces": INTERFACES_FILE, + "tasks": TASKS_FILE, + } + return [ + {"label": label, "path": str(path), "status": "available" if path.exists() else "missing"} + for label, path in paths.items() + ] + + +def _write_plan_report( + states: list[StageState], + *, + mode: str, + elapsed: float | None = None, + post_steps: list[dict[str, Any]] | None = None, +) -> str | None: + try: + done = sum(1 for s in states if s.done) + runnable = sum(1 for s in states if s.will_run) + cards = [ + {"label": "mode", "value": mode}, + {"label": "stages", "value": len(states)}, + {"label": "done", "value": done}, + {"label": "to run", "value": runnable}, + {"label": "skipped", "value": len(states) - runnable}, + ] + if elapsed is not None: + cards.append({"label": "elapsed", "value": f"{elapsed:.1f}s"}) + if post_steps is not None: + cards.append({"label": "post steps", "value": len(post_steps)}) + stage_rows = _stage_rows(states) + artifact_rows = _plan_artifacts() + steps = [ + StepEvent(name=row["name"], status=row["status"], reason=row.get("reason", "")) + for row in stage_rows + ] + for post in post_steps or []: + steps.append(StepEvent( + name=post.get("name", "post-step"), + status="ok" if post.get("returncode") == 0 else "warning", + reason=f"exit {post.get('returncode')}", + )) + report_path = write_command_report(CommandRun( + command="plan", + title="CoderMind plan Explain View", + status=mode, + summary=cards, + steps=steps, + artifacts=[ + ArtifactEvent(label=row["label"], path=row["path"], status=row.get("status")) + for row in artifact_rows + ], + verification=[ + VerificationEvent(name=s.stage.name, status=s.type, detail=s.message or s.reason) + for s in states + ], + evidence={ + "mode": mode, + "elapsed": elapsed, + "stages": stage_rows, + "post_steps": post_steps or [], + "artifacts": artifact_rows, + }, + )) + return str(report_path) + except Exception: + return None + + def _format_table(states: list[StageState]) -> str: rows = ["Stage Type Done Action"] rows.append("-" * 50) @@ -322,6 +426,7 @@ def _emit_check_only_json(states: list[StageState]) -> None: done = sum(1 for s in states if s.done) total = len(states) next_pending = next((s.stage.name for s in states if not s.done), None) + report_path = _write_plan_report(states, mode="check-only") payload = { "total": total, "done": done, @@ -332,10 +437,14 @@ def _emit_check_only_json(states: list[StageState]) -> None: "type": s.type, "message": s.message, "done": s.done, + "will_run": s.will_run, + "reason": s.reason, } for s in states ], } + if report_path: + payload["report_path"] = report_path print(json.dumps(payload, indent=2)) @@ -436,6 +545,9 @@ def main(argv: Optional[list[str]] = None) -> int: _emit_check_only_json(states) else: _print_probe_summary(states) + report_path = _write_plan_report(states, mode="check-only") + if report_path: + print(f"Report: {report_path}") return 0 # --- Step: prerequisite check ----------------------------------------- @@ -521,15 +633,20 @@ def main(argv: Optional[list[str]] = None) -> int: # --- Step: post-pipeline helpers -------------------------------------- print() print("Running post-pipeline helpers ...") + post_results: list[dict[str, Any]] = [] for post in POST_STEPS: print(f"▶ {post}") rc = _run_stage(invoker, post, []) + post_results.append({"name": post, "returncode": rc}) if rc != 0: print(f" warning: {post} exited with {rc} (continuing)") total_elapsed = time.monotonic() - started + report_path = _write_plan_report(states, mode="complete", elapsed=total_elapsed, post_steps=post_results) print() print(f"Plan complete in {total_elapsed:.1f}s.") + if report_path: + print(f"Report: {report_path}") print("Next: `/cmind.code_gen` to generate source code.") print("Graph: see the 'Writing visualization to:' line above for the generated HTML path.") return 0 diff --git a/CoderMind/scripts/rpg/dep_graph.py b/CoderMind/scripts/rpg/dep_graph.py index 093228f..5397895 100644 --- a/CoderMind/scripts/rpg/dep_graph.py +++ b/CoderMind/scripts/rpg/dep_graph.py @@ -214,6 +214,7 @@ def __init__(self, repo_dir: str): _DERIVED_FIELDS = frozenset({ "imports_from", "calls", "called_by", "inherits", "inherited_by", + "rpg_nodes", }) # ------------------------------------------------------------------ diff --git a/CoderMind/scripts/rpg/graph_query.py b/CoderMind/scripts/rpg/graph_query.py index 734d07f..cc119a6 100644 --- a/CoderMind/scripts/rpg/graph_query.py +++ b/CoderMind/scripts/rpg/graph_query.py @@ -135,18 +135,26 @@ def from_files(cls, rpg_path: str, dep_graph_path: str = "") -> "GraphQueryEngin # Helpers # ------------------------------------------------------------------ - def _normalize_path(self, meta_path: str) -> str: - """Strip redundant code_dir prefix from RPG meta.path. - - For legacy data where ``_dep_graph_code_dir`` was ``"repo"``, - this converts e.g. ``repo/routes/auth.py`` to ``routes/auth.py`` - so it aligns with dep_graph node IDs. In the unified - workspace==repo layout the prefix is empty and this is a no-op. - """ + def _normalize_path(self, meta_path: Any) -> Any: + """Strip redundant code_dir prefix from RPG meta.path.""" + if isinstance(meta_path, list): + return [self._normalize_path(path) for path in meta_path] + if not isinstance(meta_path, str): + return meta_path if self._code_dir_prefix and meta_path.startswith(self._code_dir_prefix): return meta_path[len(self._code_dir_prefix):] return meta_path + def _path_search_text(self, meta_path: Any) -> str: + normalized_path = self._normalize_path(meta_path) + if isinstance(normalized_path, list): + return " ".join( + self._path_search_text(path) for path in normalized_path + ) + if isinstance(normalized_path, str): + return normalized_path.split(":", 1)[0].lower() + return str(normalized_path).lower() if normalized_path else "" + def _get_feature_path(self, node_id: str) -> str: """Build ancestor chain for an RPG node (excluding repo root).""" parts: list[str] = [] @@ -306,8 +314,8 @@ def _search_rpg_tree(self, query: str, top_k: int) -> List[Dict[str, Any]]: elif query in name.lower(): score = 75 else: - file_part = meta_path.split(":")[0] if ":" in meta_path else meta_path - if query in file_part.lower(): + path_text = self._path_search_text(meta_path) + if path_text and query in path_text: score = 60 elif query in nid.lower(): score = 55 diff --git a/CoderMind/scripts/rpg_edit/apply.py b/CoderMind/scripts/rpg_edit/apply.py index 2421576..73877f5 100644 --- a/CoderMind/scripts/rpg_edit/apply.py +++ b/CoderMind/scripts/rpg_edit/apply.py @@ -8,6 +8,7 @@ import argparse import json +import shlex import shutil import subprocess import sys @@ -21,7 +22,15 @@ if str(SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(SCRIPTS_DIR)) -from common.paths import REPO_RPG_FILE, DEP_GRAPH_FILE, REPO_DIR, RPG_EDIT_PLAN_FILE # noqa: E402 +from common.paths import ( # noqa: E402 + REPO_RPG_FILE, + DEP_GRAPH_FILE, + REPO_DIR, + RPG_EDIT_PLAN_FILE, + RPG_EDIT_APPLY_RESULT_FILE, + cmd_for, +) +from common.git_utils import read_head # noqa: E402 def _backup(rpg_path: Path, dep_graph_path: Path, ts: str) -> Dict[str, str]: @@ -50,6 +59,77 @@ def _rollback(backups: Dict[str, str], rpg_path: Path, dep_graph_path: Path) -> shutil.copy2(backups["dep_graph"], dep_graph_path) +def _is_rpg_edit_branch(branch: Any) -> bool: + return isinstance(branch, str) and branch.startswith("rpg-edit/") + + +def _rollback_command(timestamp: str | None, before_state: dict | None) -> str | None: + if not timestamp: + return None + command = f"{cmd_for('rpg_edit/apply.py')} --rollback {shlex.quote(str(timestamp))}" + branch = before_state.get("head_branch") if isinstance(before_state, dict) else None + if _is_rpg_edit_branch(branch): + command += f" --rollback-branch {shlex.quote(str(branch))}" + return command + + +def _rollback_path(backups: Dict[str, str]) -> str | None: + return backups.get("rpg") or backups.get("dep_graph") + + +def _persist_apply_result(result: Dict[str, Any]) -> None: + RPG_EDIT_APPLY_RESULT_FILE.parent.mkdir(parents=True, exist_ok=True) + RPG_EDIT_APPLY_RESULT_FILE.write_text( + json.dumps(result, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + +def _record_apply_result( + result: Dict[str, Any], + *, + backup_timestamp: str | None = None, + backups: Dict[str, str] | None = None, + applied_features: list | None = None, + dep_graph_refreshed: bool | None = None, + before_state: dict | None = None, + confirmed: bool | None = None, +) -> Dict[str, Any]: + backups = backups or {} + preserved: Dict[str, Any] = {} + if backup_timestamp is not None and RPG_EDIT_APPLY_RESULT_FILE.exists(): + try: + previous = json.loads(RPG_EDIT_APPLY_RESULT_FILE.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + previous = {} + if isinstance(previous, dict) and str(previous.get("backup_timestamp")) == str(backup_timestamp): + preserved = previous + for key in ("applied_features", "backups", "confirmed", "before_state", "rollback_command", "rollback_path"): + previous_value = preserved.get(key) + if previous_value in (None, "", [], {}): + continue + if result.get(key) in (None, "", [], {}): + result[key] = previous_value + if not backups and isinstance(result.get("backups"), dict): + backups = result["backups"] + if backup_timestamp is not None: + result.setdefault("backup_timestamp", backup_timestamp) + result.setdefault("rollback_command", _rollback_command(backup_timestamp, before_state)) + result.setdefault("backups", backups) + result.setdefault("applied_features", applied_features or []) + if dep_graph_refreshed is not None: + result.setdefault("dep_graph_refreshed", dep_graph_refreshed) + rollback_path = _rollback_path(backups) + if rollback_path: + result.setdefault("rollback_path", rollback_path) + if before_state is not None: + result.setdefault("before_state", before_state) + if confirmed is not None: + result.setdefault("confirmed", confirmed) + _persist_apply_result(result) + return result + + def apply_feature_changes(svc, changes: list) -> list: """Apply feature_changes to the RPG in memory. @@ -128,6 +208,8 @@ def main(): parser.add_argument("--backup-ts", type=str, default=None, help="Reuse existing backup timestamp (skip new backup)") parser.add_argument("--skip-tests", action="store_true") + parser.add_argument("--confirmed", action="store_const", const=True, default=None, + help="Record that the apply boundary was explicitly confirmed") parser.add_argument("--rollback", type=str, default=None, help="Rollback to a previous timestamp backup") parser.add_argument("--rollback-branch", type=str, default=None, @@ -145,6 +227,8 @@ def main(): from common.logging_setup import setup_file_logging setup_file_logging("rpg_edit") + before_state = read_head(REPO_DIR) + # Handle rollback if args.rollback: rpg_backup = args.rpg.with_suffix(f".before-edit-{args.rollback}.json") @@ -180,10 +264,23 @@ def main(): "message": f"git invocation failed: {exc}", } + rollback_backups = {} + if rpg_backup.exists(): + rollback_backups["rpg"] = str(rpg_backup) + if dg_backup.exists(): + rollback_backups["dep_graph"] = str(dg_backup) result = {"type": "rollback", "restored": restored, "timestamp": args.rollback} if branch_result: result["branch"] = branch_result + _record_apply_result( + result, + backup_timestamp=args.rollback, + backups=rollback_backups, + dep_graph_refreshed=False, + before_state=before_state, + confirmed=args.confirmed, + ) print(json.dumps(result, indent=2) if args.json else f"Rolled back: {restored}" + (f"; branch={branch_result}" if branch_result else "")) @@ -192,6 +289,12 @@ def main(): # Load plan if not args.plan.exists(): result = {"type": "error", "message": f"Plan not found: {args.plan}"} + _record_apply_result( + result, + dep_graph_refreshed=False, + before_state=before_state, + confirmed=args.confirmed, + ) print(json.dumps(result) if args.json else f"Error: {result['message']}") return 1 @@ -214,6 +317,7 @@ def main(): # --- Phase: rpg-only or all → apply feature_changes --- applied_features = [] + dep_graph_refreshed = False if args.phase in ("rpg-only", "all"): feature_changes = plan.get("feature_changes", []) applied_features = apply_feature_changes(svc, feature_changes) if feature_changes else [] @@ -226,12 +330,20 @@ def main(): "backup_timestamp": ts, "backups": backups, } + _record_apply_result( + result, + backup_timestamp=ts, + backups=backups, + applied_features=applied_features, + dep_graph_refreshed=dep_graph_refreshed, + before_state=before_state, + confirmed=args.confirmed, + ) print(json.dumps(result, indent=2) if args.json else f"RPG updated ({len(applied_features)} features). Backup: {ts}") return 0 # --- Phase: dep-refresh or all → refresh dep_graph --- - dep_graph_refreshed = False if args.phase in ("dep-refresh", "all"): # Workspace root is the project repo root. Explicit ``--repo`` # still wins for tests / brownfield setups where the code lives @@ -252,6 +364,15 @@ def main(): "message": f"dep_graph refresh failed: {exc}", "backup_timestamp": ts, } + _record_apply_result( + result, + backup_timestamp=ts, + backups=backups, + applied_features=applied_features, + dep_graph_refreshed=dep_graph_refreshed, + before_state=before_state, + confirmed=args.confirmed, + ) print(json.dumps(result, indent=2) if args.json else f"Error: {result['message']}") return 1 @@ -264,6 +385,15 @@ def main(): "dep_graph_refreshed": dep_graph_refreshed, "backup_timestamp": ts, } + _record_apply_result( + result, + backup_timestamp=ts, + backups=backups, + applied_features=applied_features, + dep_graph_refreshed=dep_graph_refreshed, + before_state=before_state, + confirmed=args.confirmed, + ) print(json.dumps(result, indent=2) if args.json else f"dep_graph refreshed: {dep_graph_refreshed}. Backup: {ts}") return 0 @@ -293,9 +423,19 @@ def main(): "type": "test_failed", "applied_features": applied_features, "test_output": test_result["output"], + "test_result": test_result, "rolled_back": True, "backup_timestamp": ts, } + _record_apply_result( + result, + backup_timestamp=ts, + backups=backups, + applied_features=applied_features, + dep_graph_refreshed=dep_graph_refreshed, + before_state=before_state, + confirmed=args.confirmed, + ) print(json.dumps(result, indent=2) if args.json else f"Tests failed. Rolled back to {ts}.") return 1 @@ -309,6 +449,15 @@ def main(): "backup_timestamp": ts, "backups": backups, } + _record_apply_result( + result, + backup_timestamp=ts, + backups=backups, + applied_features=applied_features, + dep_graph_refreshed=dep_graph_refreshed, + before_state=before_state, + confirmed=args.confirmed, + ) print(json.dumps(result, indent=2) if args.json else "EditPlan applied successfully.") return 0 diff --git a/CoderMind/scripts/rpg_edit/code.py b/CoderMind/scripts/rpg_edit/code.py index c070be7..6d0172e 100644 --- a/CoderMind/scripts/rpg_edit/code.py +++ b/CoderMind/scripts/rpg_edit/code.py @@ -43,6 +43,7 @@ RPG_FILE, REPO_DIR, RPG_EDIT_PLAN_FILE, + RPG_EDIT_CODE_RESULT_FILE, DATA_DIR, WORKSPACE_ROOT, cmd_for, @@ -52,6 +53,14 @@ logger = logging.getLogger(__name__) +def _write_code_result(result: Dict[str, Any]) -> None: + RPG_EDIT_CODE_RESULT_FILE.parent.mkdir(parents=True, exist_ok=True) + RPG_EDIT_CODE_RESULT_FILE.write_text( + json.dumps(result, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + # --------------------------------------------------------------------------- # Prompt templates # --------------------------------------------------------------------------- @@ -584,6 +593,7 @@ def apply_code_changes( fp = c.get("file_path") if fp and fp not in done_files: done_files.append(fp) + last_error = None iter_info["detail"] = None break elif status == "partial": @@ -684,6 +694,7 @@ def main() -> int: max_iterations=args.max_iterations, timeout=args.timeout, ) + _write_code_result(result) if args.json: print(json.dumps(result, indent=2, ensure_ascii=False)) diff --git a/CoderMind/scripts/rpg_edit/locate.py b/CoderMind/scripts/rpg_edit/locate.py index 082b819..43492c6 100644 --- a/CoderMind/scripts/rpg_edit/locate.py +++ b/CoderMind/scripts/rpg_edit/locate.py @@ -20,7 +20,15 @@ if str(SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(SCRIPTS_DIR)) -from common.paths import REPO_RPG_FILE # noqa: E402 +from common.paths import REPO_RPG_FILE, RPG_EDIT_LOCATE_FILE # noqa: E402 + + +def _write_locate_result(result: dict) -> None: + RPG_EDIT_LOCATE_FILE.parent.mkdir(parents=True, exist_ok=True) + RPG_EDIT_LOCATE_FILE.write_text( + json.dumps(result, indent=2, ensure_ascii=False), + encoding="utf-8", + ) def _build_tree_summary(svc, max_depth: int = 3, max_lines: int = 150) -> List[str]: @@ -198,6 +206,7 @@ def main(): # when search results are poor (e.g. editing features that don't exist yet). tree_lines = _build_tree_summary(svc) output["tree_summary"] = tree_lines + _write_locate_result(output) if args.json: print(json.dumps(output, indent=2, ensure_ascii=False)) diff --git a/CoderMind/scripts/rpg_edit/review.py b/CoderMind/scripts/rpg_edit/review.py index 85b8386..082971d 100644 --- a/CoderMind/scripts/rpg_edit/review.py +++ b/CoderMind/scripts/rpg_edit/review.py @@ -21,12 +21,14 @@ import argparse import json import logging +import re import shutil import sys import time +import uuid from pathlib import Path from string import Template -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Mapping, Optional, Tuple # This file lives in ``scripts/rpg_edit/``; go up two levels to land # on ``scripts/`` so ``common.*``, ``rpg.*`` etc. import cleanly. @@ -34,10 +36,2346 @@ if str(SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(SCRIPTS_DIR)) -from common.paths import REPO_DIR, cmd_for, RPG_EDIT_PLAN_FILE, RPG_EDIT_IMPACT_FILE # noqa: E402 +from common.diff_ranges import ( # noqa: E402 + changed_line_ranges_by_file, + is_file_level_node, + line_range_from_mapping, + row_overlaps_changed_lines, +) +from common.paths import ( # noqa: E402 + REPO_DIR, + REPORTS_DIR, + REPO_RPG_FILE, + cmd_for, + RPG_EDIT_PLAN_FILE, + RPG_EDIT_IMPACT_FILE, + RPG_EDIT_VALIDATE_FILE, + RPG_EDIT_LOCATE_FILE, + RPG_EDIT_CODE_RESULT_FILE, + RPG_EDIT_APPLY_RESULT_FILE, + RPG_EDIT_REVIEW_RESULT_FILE, +) +from common.run_events import ( # noqa: E402 + ArtifactEvent, + CodeDeltaEvent, + CommandRun, + DepGraphDeltaEvent, + RPGDeltaEvent, + RetrievalEvent, + StepEvent, + UserDecisionEvent, + VerificationEvent, +) +from common.git_utils import file_diffs_between, read_head # noqa: E402 +from common.run_report import write_command_report # noqa: E402 logger = logging.getLogger(__name__) + +def _write_review_result(result: Dict[str, Any]) -> None: + RPG_EDIT_REVIEW_RESULT_FILE.parent.mkdir(parents=True, exist_ok=True) + RPG_EDIT_REVIEW_RESULT_FILE.write_text( + json.dumps(result, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + + +def _load_json_artifact(path: Optional[Path]) -> Any: + if path is None or not path.exists(): + return None + try: + return json.loads(path.read_text(encoding="utf-8")) + except Exception as exc: + return {"_error": str(exc), "_path": str(path)} + + +def _load_review_artifacts(plan_path: Path, impact_path: Optional[Path]) -> Dict[str, Any]: + return { + "validate": _load_json_artifact(RPG_EDIT_VALIDATE_FILE), + "locate": _load_json_artifact(RPG_EDIT_LOCATE_FILE), + "plan": _load_json_artifact(plan_path), + "impact": _load_json_artifact(impact_path), + "code_result": _load_json_artifact(RPG_EDIT_CODE_RESULT_FILE), + "apply_result": _load_json_artifact(RPG_EDIT_APPLY_RESULT_FILE), + } + + +_REPORT_SCOPES = {"final", "internal", "none"} + + +def _normalize_report_scope(report_scope: str) -> str: + scope = str(report_scope or "final").strip().lower() + if scope not in _REPORT_SCOPES: + raise ValueError(f"report_scope must be one of {sorted(_REPORT_SCOPES)}, got {report_scope!r}") + return scope + + +def _report_target_dir(report_scope: str, report_dir: Optional[Path]) -> Path: + base_dir = Path(report_dir) if report_dir is not None else REPORTS_DIR + return base_dir / "internal" if report_scope == "internal" else base_dir + + +def _report_filename_slug(value: str) -> str: + slug = re.sub(r"[^A-Za-z0-9_.-]+", "_", value).strip("._-") + return slug or "command" + + +def _report_timestamp() -> str: + return time.strftime("%Y%m%dT%H%M%SZ", time.gmtime()) + + +def _expected_report_path(report_dir: Path, report_timestamp: str) -> Path: + return report_dir / f"cmind_run_rpg_edit_{_report_filename_slug(report_timestamp)}.html" + + +def _load_existing_review_result() -> Dict[str, Any]: + result = _load_json_artifact(RPG_EDIT_REVIEW_RESULT_FILE) + return result if isinstance(result, dict) else {} + + +def _existing_internal_report_paths(result: Dict[str, Any]) -> List[str]: + paths = _listify(result.get("internal_report_paths")) + if result.get("report_scope") == "internal" and result.get("report_path"): + paths.extend(_listify(result.get("report_path"))) + return _ordered_unique(paths) + + +def _artifact_links( + plan_path: Path, + impact_path: Optional[Path], + internal_report_paths: Optional[List[str]] = None, +) -> List[Dict[str, Any]]: + paths = { + "validate": RPG_EDIT_VALIDATE_FILE, + "locate": RPG_EDIT_LOCATE_FILE, + "plan": plan_path, + "impact": impact_path, + "code_result": RPG_EDIT_CODE_RESULT_FILE, + "apply_result": RPG_EDIT_APPLY_RESULT_FILE, + "review_result": RPG_EDIT_REVIEW_RESULT_FILE, + } + links: List[Dict[str, Any]] = [] + for label, path in paths.items(): + if path is None: + continue + status = "available" if path.exists() or label == "review_result" else "missing" + links.append({"label": label, "path": str(path), "status": status}) + for index, path in enumerate(_ordered_unique(internal_report_paths or []), start=1): + try: + status = "available" if Path(path).exists() else "missing" + except (OSError, ValueError): + status = "missing" + links.append({"label": f"internal_report_{index}", "path": path, "status": status}) + return links + + +def _impact_results(artifacts: Dict[str, Any]) -> Dict[str, Any]: + impact = artifacts.get("impact") if isinstance(artifacts.get("impact"), dict) else {} + results = impact.get("results") if isinstance(impact.get("results"), dict) else {} + return results + + +def _selected_candidate_rows(artifacts: Dict[str, Any]) -> List[Dict[str, Any]]: + locate = artifacts.get("locate") if isinstance(artifacts.get("locate"), dict) else {} + plan = artifacts.get("plan") if isinstance(artifacts.get("plan"), dict) else {} + affected = [node_id for node_id in plan.get("affected_nodes") or [] if node_id] + candidates = [c for c in locate.get("results") or [] if isinstance(c, dict)] + if not affected: + return candidates + + impact_results = _impact_results(artifacts) + candidates_by_id = {c.get("node_id"): c for c in candidates if c.get("node_id") in affected} + rows: List[Dict[str, Any]] = [] + for node_id in affected: + impact = impact_results.get(node_id) if isinstance(impact_results.get(node_id), dict) else {} + located = candidates_by_id.get(node_id) + candidate = dict(located or {"node_id": node_id}) + if located: + candidate.setdefault("locate_state", "selected") + else: + candidate["locate_state"] = "missing" + candidate["reason_state"] = "reconstructed_from_plan_impact" + if impact: + candidate.setdefault("name", impact.get("name", "")) + if not candidate.get("dep_nodes"): + candidate["dep_nodes"] = impact.get("dep_nodes") or [] + candidate["dep_nodes_source"] = "impact" + if not candidate.get("status") and (impact.get("error") or impact.get("message")): + candidate["status"] = impact.get("error") or impact.get("message") + rows.append(candidate) + return rows + + +def _listify(value: Any) -> List[Any]: + if value in (None, ""): + return [] + if isinstance(value, (list, tuple, set)): + return [item for item in value if item not in (None, "")] + return [value] + + +def _ordered_unique(values: List[Any]) -> List[str]: + seen: set[str] = set() + ordered: List[str] = [] + for value in values: + if value in (None, ""): + continue + text = str(value) + if text in seen: + continue + seen.add(text) + ordered.append(text) + return ordered + + +def _candidate_dep_nodes(candidate: Dict[str, Any], impact: Dict[str, Any]) -> List[str]: + return _ordered_unique(_listify(candidate.get("dep_nodes")) + _listify(impact.get("dep_nodes"))) + + +def _retrieval_hit_reason(candidate: Dict[str, Any], impact: Dict[str, Any]) -> str: + parts: List[str] = [] + if candidate.get("locate_state") == "missing": + parts.append("locate missing; reconstructed from plan/impact") + elif candidate.get("score") not in (None, ""): + parts.append(f"locate score={candidate.get('score')}") + elif candidate.get("node_id"): + parts.append("selected feature") + if candidate.get("feature_path"): + parts.append(f"feature path={candidate.get('feature_path')}") + dep_count = len(_candidate_dep_nodes(candidate, impact)) + if dep_count: + parts.append(f"{dep_count} mapped code relations ({dep_count} dep nodes)") + else: + parts.append("missing dep_graph mapping") + if not impact: + parts.append("missing impact result") + elif impact.get("error"): + parts.append(f"impact error={impact.get('error')}") + elif impact.get("message"): + parts.append(f"impact state={impact.get('message')}") + summary = impact.get("impact_summary") if isinstance(impact.get("impact_summary"), dict) else {} + callers = summary.get("total_callers", len(impact.get("callers") or [])) + callees = summary.get("total_callees", len(impact.get("callees") or [])) + inheritance = summary.get("total_inheritance", len(impact.get("inheritance") or [])) + files = summary.get("affected_file_count", len(impact.get("affected_files") or [])) + if callers or callees or inheritance or files: + parts.append(f"impact callers={callers}, affected_files={files}") + if callees or inheritance: + parts.append(f"impact callees={callees}, inheritance={inheritance}") + return "; ".join(parts) or "selected by review plan" + + +def _retrieval_rows(artifacts: Dict[str, Any], candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + locate = artifacts.get("locate") if isinstance(artifacts.get("locate"), dict) else {} + impact_results = _impact_results(artifacts) + if locate or candidates: + hits: List[Dict[str, Any]] = [] + for candidate in candidates: + node_id = candidate.get("node_id") + impact = impact_results.get(node_id) if isinstance(impact_results.get(node_id), dict) else {} + dep_nodes = _candidate_dep_nodes(candidate, impact) + locate_state = candidate.get("locate_state") or ("selected" if node_id else "missing") + impact_state = "available" if impact else "missing" + if impact.get("error"): + impact_state = "error" + elif impact and not dep_nodes: + impact_state = "missing_mapping" + hits.append({ + "node_id": node_id, + "name": candidate.get("name"), + "path": candidate.get("path") or candidate.get("meta_path"), + "score": candidate.get("score"), + "locate_state": locate_state, + "impact_state": impact_state, + "mapping_state": "mapped" if dep_nodes else "missing_mapping", + "mapped_code_relations": len(dep_nodes), + "reason": _retrieval_hit_reason(candidate, impact), + }) + rows.append({ + "query": locate.get("query", ""), + "tool": str(RPG_EDIT_LOCATE_FILE), + "reason": f"{len(hits)} selected feature groups from {len(locate.get('results') or [])} locate candidates", + "hits": hits, + }) + if impact_results: + hits = [] + for node_id, impact in impact_results.items(): + impact = impact if isinstance(impact, dict) else {} + dep_nodes = _candidate_dep_nodes({"node_id": node_id}, impact) + hits.append({ + "node_id": node_id, + "name": impact.get("name"), + "impact_state": "available" if dep_nodes else "missing_mapping", + "mapped_code_relations": len(dep_nodes), + "reason": _retrieval_hit_reason({"node_id": node_id}, impact), + }) + rows.append({ + "query": ", ".join(str(node_id) for node_id in impact_results), + "tool": str(RPG_EDIT_IMPACT_FILE), + "reason": f"{len(impact_results)} impact result sets with mapped code relations", + "hits": hits, + }) + return rows + + +def _code_delta_rows(artifacts: Dict[str, Any]) -> List[Dict[str, Any]]: + code_result = artifacts.get("code_result") if isinstance(artifacts.get("code_result"), dict) else {} + files = [str(path) for path in _listify(code_result.get("files_modified"))] + commit_sha = code_result.get("commit_sha") + rows: List[Dict[str, Any]] = [] + if commit_sha: + try: + rows = file_diffs_between( + REPO_DIR, + to_commit=str(commit_sha), + files=files or None, + py_only=False, + ) + except Exception as exc: + rows = [{"file": path, "change_type": "modify", "diff": "", "error": str(exc)} for path in files] + seen = {row.get("file") for row in rows} + for path in files: + if path not in seen: + rows.append({"file": path, "change_type": "modify", "diff": ""}) + return rows + + +def _dep_node_path(dep_id: Any) -> str: + if dep_id in (None, ""): + return "" + dep_id_text = str(dep_id) + return dep_id_text.split(":", 1)[0] if ":" in dep_id_text else dep_id_text + + +def _modified_dep_ids( + dep_ids: List[str], + relation_by_dep: Mapping[str, Dict[str, Any]], + current_dep_nodes: Mapping[str, Dict[str, Any]], + changed_ranges: Mapping[str, List[Tuple[int, int]]], + changed_files: List[str], +) -> List[str]: + changed_file_set = set(changed_files) + ranged_matches: List[str] = [] + fallback_matches: List[str] = [] + for dep_id in dep_ids: + relation = relation_by_dep.get(dep_id, {}) + current = current_dep_nodes.get(dep_id, {}) + row = {**relation, **current} + path = str(current.get("path") or relation.get("path") or _dep_node_path(dep_id)) + if path not in changed_file_set: + continue + if line_range_from_mapping(row): + if row_overlaps_changed_lines(row, path, changed_ranges): + ranged_matches.append(dep_id) + elif is_file_level_node(dep_id, row, path): + fallback_matches.append(dep_id) + return _ordered_unique(ranged_matches or fallback_matches[:1]) + + +def _mapped_code_relations(candidate: Dict[str, Any], impact: Dict[str, Any]) -> List[Dict[str, Any]]: + candidate_dep = {str(dep_id) for dep_id in _listify(candidate.get("dep_nodes"))} + impact_dep = {str(dep_id) for dep_id in _listify(impact.get("dep_nodes"))} + candidate_dep_source = candidate.get("dep_nodes_source") + rows: List[Dict[str, Any]] = [] + for dep_id in _candidate_dep_nodes(candidate, impact): + sources: List[str] = [] + if dep_id in candidate_dep and candidate_dep_source != "impact": + sources.append("locate") + if dep_id in impact_dep or (dep_id in candidate_dep and candidate_dep_source == "impact"): + sources.append("impact") + rows.append({ + "node_id": dep_id, + "dep_node_id": dep_id, + "source_feature": candidate.get("node_id"), + "path": _dep_node_path(dep_id) or candidate.get("meta_path") or candidate.get("path"), + "relation": "feature_to_dep", + "source": "+".join(sources) or "selected_feature", + "status": "mapped", + }) + return rows + + +def _dep_node_rows(candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + for candidate in candidates: + rows.extend(_mapped_code_relations(candidate, {})) + return rows + + +def _code_delta_file(delta: Dict[str, Any]) -> str: + return str(delta.get("file") or delta.get("path") or "") + + +def _diff_anchor_slug(value: str) -> str: + return re.sub(r"[^A-Za-z0-9_.-]+", "_", value).strip("._-") or "change" + + +def _diff_anchor_map(code_deltas: List[Dict[str, Any]]) -> Dict[str, str]: + anchors: Dict[str, str] = {} + used: Dict[str, int] = {} + for index, delta in enumerate(code_deltas, start=1): + file_path = _code_delta_file(delta) or f"change-{index}" + base = _diff_anchor_slug(f"diff-{file_path}") + count = used.get(base, 0) + 1 + used[base] = count + anchor = base if count == 1 else f"{base}-{count}" + if file_path not in anchors: + anchors[file_path] = anchor + return anchors + + +def _changed_file_refs(files: List[Any], anchors: Dict[str, str]) -> List[Dict[str, Any]]: + refs: List[Dict[str, Any]] = [] + for file_path in _ordered_unique(files): + row = {"path": file_path} + _set_if_present(row, "diff_anchor", anchors.get(file_path)) + refs.append(row) + return refs + + +def _node_state(row: Dict[str, Any], default: str = "available") -> str: + status = row.get("state") or row.get("status") or row.get("mapping_status") + if status in (None, ""): + return default + if status == "missing": + return "missing_mapping" + return str(status) + + +def _node_link_id(kind: str, node_id: Any) -> str: + return _slug_id(kind, node_id) + + +def _edge_endpoint_link_id(node_id: Any, rpg_nodes: Dict[str, Dict[str, Any]], code_nodes: Dict[str, Dict[str, Any]]) -> str: + node_text = str(node_id or "") + if node_text in rpg_nodes: + return str(rpg_nodes[node_text].get("link_id") or _node_link_id("rpg", node_text)) + if node_text in code_nodes: + return str(code_nodes[node_text].get("link_id") or _node_link_id("code", node_text)) + return _node_link_id("context", node_text) + + +def _warning_link_fields(warning: Dict[str, Any], rpg_nodes: Dict[str, Dict[str, Any]], code_nodes: Dict[str, Dict[str, Any]]) -> Dict[str, Any]: + row = dict(warning) + node_id = warning.get("node_id") or warning.get("rpg_node_id") + dep_id = warning.get("dep_node_id") or warning.get("code_node_id") + if node_id not in (None, "") and str(node_id) in rpg_nodes: + row["node_link_id"] = _edge_endpoint_link_id(node_id, rpg_nodes, code_nodes) + if dep_id not in (None, "") and str(dep_id) in code_nodes: + row["code_link_id"] = _edge_endpoint_link_id(dep_id, rpg_nodes, code_nodes) + return row + + +def _hierarchy_segments(value: Any) -> List[str]: + if isinstance(value, (list, tuple)): + return [str(item) for item in value if item not in (None, "")] + if value in (None, ""): + return [] + text = str(value) + separator = " / " if " / " in text else "/" + return [part.strip() for part in text.split(separator) if part.strip()] + + +def _hierarchy_child(parent: Dict[str, Any], child_id: str, name: str, kind: str) -> Dict[str, Any]: + children = parent.setdefault("children", []) + for child in children: + if isinstance(child, dict) and child.get("id") == child_id: + return child + child = {"id": child_id, "name": name, "kind": kind, "children": []} + children.append(child) + return child + + +def _append_hierarchy_leaf(parent: Dict[str, Any], leaf: Dict[str, Any]) -> None: + children = parent.setdefault("children", []) + leaf_id = leaf.get("id") + if any(isinstance(child, dict) and child.get("id") == leaf_id for child in children): + return + children.append(leaf) + + +def _add_hierarchy_path(root: Dict[str, Any], parts: List[str], leaf: Dict[str, Any], group_kind: str) -> None: + parent = root + trail: List[str] = [] + for part in parts: + trail.append(part) + parent = _hierarchy_child(parent, _node_link_id(group_kind, "/".join(trail)), part, group_kind) + _append_hierarchy_leaf(parent, leaf) + + +def _semantic_hierarchy_parts(node: Dict[str, Any]) -> List[str]: + for key in ("breadcrumb_path", "feature_path"): + parts = _hierarchy_segments(node.get(key)) + if parts: + return parts[:-1] if len(parts) > 1 else [] + return [] + + +def _code_hierarchy_parts(node: Dict[str, Any]) -> List[str]: + for key in ("path", "module", "file"): + parts = _hierarchy_segments(node.get(key)) + if parts: + return parts[:-1] + return [] + + +def _focused_graph_hierarchy( + semantic_nodes: List[Dict[str, Any]], + code_nodes: List[Dict[str, Any]], + mappings: List[Dict[str, Any]], + edges: List[Dict[str, Any]], + hidden_counts: Dict[str, Any], + warnings: List[Dict[str, Any]], +) -> Dict[str, Any]: + root: Dict[str, Any] = { + "id": "focused-graph-root", + "name": "Focused graph", + "kind": "root", + "feature_name": "Focused graph", + "feature_path": "Focused graph", + "meta": {"hidden_counts": hidden_counts, "warnings": len(warnings), "edges": len(edges)}, + "children": [], + } + code_by_id = { + str(node.get("node_id") or node.get("dep_node_id")): node + for node in code_nodes + if (node.get("node_id") or node.get("dep_node_id")) not in (None, "") + } + semantic_link_by_id = { + str(node.get("node_id")): str(node.get("link_id") or _node_link_id("rpg", node.get("node_id"))) + for node in semantic_nodes + if node.get("node_id") not in (None, "") + } + code_link_by_id = { + code_id: str(node.get("link_id") or _node_link_id("code", code_id)) + for code_id, node in code_by_id.items() + } + mapped_ids_by_rpg: Dict[str, List[str]] = {} + for mapping in mappings: + rpg_id = mapping.get("rpg_node_id") or mapping.get("node_id") + code_id = mapping.get("code_node_id") or mapping.get("dep_node_id") + if rpg_id not in (None, "") and code_id not in (None, ""): + mapped_ids_by_rpg.setdefault(str(rpg_id), []).append(str(code_id)) + + def code_refs_for(node: Dict[str, Any]) -> List[Dict[str, Any]]: + rpg_id = str(node.get("node_id") or "") + refs: List[Dict[str, Any]] = [] + seen: set[str] = set() + + def append_ref(ref: Dict[str, Any]) -> None: + key = str(ref.get("node_id") or ref.get("link_id") or ref.get("path") or ref.get("symbol") or "") + if not key or key in seen: + return + seen.add(key) + refs.append(ref) + + for item in _listify(node.get("mapped_code")): + if not isinstance(item, dict): + continue + code_id = item.get("node_id") or item.get("dep_node_id") + code_id_text = str(code_id or "") + code = code_by_id.get(code_id_text, {}) + path = item.get("path") or code.get("path") or item.get("file") or code.get("file") or code.get("module") or _dep_node_path(code_id_text) + symbol = item.get("symbol") or item.get("name") or code.get("symbol") or code.get("name") or _symbol_from_dep(code_id_text, code) + row: Dict[str, Any] = { + "node_id": code_id_text, + "link_id": item.get("link_id") or code.get("link_id") or _node_link_id("code", code_id_text), + "path": path, + "symbol": symbol, + } + for key in ("type", "kind", "line_range", "state", "source"): + _set_if_present(row, key, item.get(key) or code.get(key)) + append_ref(row) + + code_ids = _ordered_unique(_listify(node.get("mapped_code_node_ids")) + mapped_ids_by_rpg.get(rpg_id, [])) + for code_id in code_ids: + code = code_by_id.get(code_id, {}) + path = code.get("path") or code.get("file") or code.get("module") or _dep_node_path(code_id) + symbol = code.get("symbol") or code.get("name") or _symbol_from_dep(code_id, code) + row = { + "node_id": code_id, + "link_id": code.get("link_id") or _node_link_id("code", code_id), + "path": path, + "symbol": symbol, + } + for key in ("type", "kind", "line_range", "state", "source"): + _set_if_present(row, key, code.get(key)) + append_ref(row) + return refs + + def merge_code_metadata(row: Dict[str, Any], refs: List[Dict[str, Any]]) -> None: + if not refs: + return + existing = [item for item in _listify(row.get("mapped_code")) if isinstance(item, dict)] + merged: List[Dict[str, Any]] = [] + seen: set[str] = set() + for ref in existing + refs: + key = str(ref.get("node_id") or ref.get("link_id") or ref.get("path") or ref.get("symbol") or "") + if not key or key in seen: + continue + seen.add(key) + merged.append(ref) + row["mapped_code"] = merged + row["mapped_code_node_ids"] = _ordered_unique([ref.get("node_id") for ref in merged]) + row["mapped_code_link_ids"] = _ordered_unique([ref.get("link_id") for ref in merged]) + row["mapped_code_paths"] = _ordered_unique([ref.get("path") for ref in merged]) + row["mapped_code_symbols"] = _ordered_unique([ref.get("symbol") for ref in merged]) + if row["mapped_code_paths"]: + row["mapped_code_path"] = row["mapped_code_paths"][0] + if row["mapped_code_symbols"]: + row["mapped_code_symbol"] = row["mapped_code_symbols"][0] + row["mapped_code_count"] = len(merged) + + def feature_path_text(node: Dict[str, Any], group_parts: List[str], feature_name: str) -> str: + for key in ("breadcrumb_path", "feature_path"): + value = node.get(key) + parts = _hierarchy_segments(value) + if parts: + return " / ".join(parts) + return " / ".join(group_parts + ([feature_name] if feature_name else [])) + + def semantic_tree_kind(node: Dict[str, Any]) -> str: + node_type = str(node.get("node_type") or node.get("type") or "").lower() + if node_type in {"feature_group", "category", "functional_area", "root"}: + return node_type + return "feature" + + def semantic_full_path_parts(node: Dict[str, Any], group_parts: List[str], feature_name: str) -> List[str]: + return _hierarchy_segments(feature_path_text(node, group_parts, feature_name)) + + semantic_node_by_path: Dict[str, Dict[str, Any]] = {} + for semantic_node in semantic_nodes: + semantic_name = str(semantic_node.get("name") or semantic_node.get("symbol") or semantic_node.get("node_id") or "") + semantic_parts = semantic_full_path_parts(semantic_node, _semantic_hierarchy_parts(semantic_node), semantic_name) + if semantic_parts: + semantic_node_by_path.setdefault(" / ".join(semantic_parts), semantic_node) + + def merge_semantic_metadata(row: Dict[str, Any], node: Dict[str, Any], feature_name: str, feature_path: str) -> None: + node_id = str(node.get("node_id") or "") + link_id = str(node.get("link_id") or _node_link_id("rpg", node_id)) + row["name"] = feature_name + row["feature_name"] = feature_name + row["feature_path"] = feature_path + row["kind"] = semantic_tree_kind(node) + if node_id: + row["node_id"] = node_id + row["aliases"] = _ordered_unique(_listify(row.get("aliases")) + [node_id, link_id, _node_link_id("feature-path", feature_path)]) + _set_if_present(row, "state", node.get("state")) + _set_if_present(row, "mapping_status", node.get("mapping_status")) + for key in ( + "type", + "node_type", + "path", + "breadcrumb", + "breadcrumb_path", + "locate_status", + "score", + "reason", + "apply_action", + "changed_files", + "hidden_counts", + "warning_types", + "source", + ): + _set_if_present(row, key, node.get(key)) + + def append_or_merge_hierarchy_leaf(parent: Dict[str, Any], leaf: Dict[str, Any]) -> None: + children = parent.setdefault("children", []) + leaf_id = leaf.get("id") + for child in children: + if not isinstance(child, dict) or child.get("id") != leaf_id: + continue + incoming_children = [item for item in _listify(leaf.get("children")) if isinstance(item, dict)] + for key, value in leaf.items(): + if key == "children" or value in (None, ""): + continue + child[key] = value + for grandchild in incoming_children: + _append_hierarchy_leaf(child, grandchild) + return + children.append(leaf) + + attached_endpoint_ids: set[str] = set() + + def make_code_leaf(ref: Dict[str, Any]) -> Dict[str, Any]: + code_id = str(ref.get("node_id") or ref.get("dep_node_id") or "") + code = code_by_id.get(code_id, {}) + path = ref.get("path") or code.get("path") or code.get("file") or code.get("module") or _dep_node_path(code_id) + symbol = ref.get("symbol") or ref.get("name") or code.get("symbol") or code.get("name") or _symbol_from_dep(code_id, code) + leaf: Dict[str, Any] = { + "id": str(ref.get("link_id") or code.get("link_id") or _node_link_id("code", code_id)), + "node_id": code_id, + "dep_node_id": code_id, + "name": symbol or path or code_id, + "symbol": symbol, + "path": path, + "kind": "code", + "state": ref.get("state") or code.get("state") or _node_state(code, "mapped"), + "aliases": _ordered_unique([code_id, ref.get("link_id"), code.get("link_id")]), + } + for key in ("type", "line_range", "source", "changed", "changed_files", "diff_anchor"): + _set_if_present(leaf, key, ref.get(key) or code.get(key)) + return leaf + + def endpoint_leaf(edge: Dict[str, Any], side: str) -> Optional[Dict[str, Any]]: + node_id = edge.get(f"{side}_node_id") + node_text = str(node_id or "") + link_id = edge.get(f"{side}_link_id") or semantic_link_by_id.get(node_text) or code_link_by_id.get(node_text) + if link_id in (None, "") and node_text: + link_id = _node_link_id("context", node_text) + link_text = str(link_id or "") + if not link_text or link_text in set(semantic_link_by_id.values()): + return None + if node_text in code_by_id and link_text == code_link_by_id.get(node_text): + return None + leaf: Dict[str, Any] = { + "id": link_text, + "node_id": node_text or link_text, + "name": edge.get(f"{side}_name") or edge.get("name") or edge.get(f"{side}_path") or edge.get("path") or node_text or "context", + "kind": "context", + "state": edge.get("state") or "context", + "relation": edge.get("relation"), + "direction": edge.get("direction"), + "source": edge.get("source") or edge.get("source_graph") or edge.get("edge_source"), + "aliases": _ordered_unique([node_text, link_text]), + } + path = edge.get(f"{side}_path") or edge.get("path") + _set_if_present(leaf, "path", path) + for key in ("reason", "source_graph", "edge_source", "relation_source"): + _set_if_present(leaf, key, edge.get(key)) + return leaf + + def append_endpoint(parent: Dict[str, Any], leaf: Dict[str, Any]) -> None: + leaf_id = str(leaf.get("id") or "") + if not leaf_id or leaf_id in attached_endpoint_ids: + return + attached_endpoint_ids.add(leaf_id) + _append_hierarchy_leaf(parent, leaf) + + def relation_endpoint_refs(node_id: str, link_id: str, code_refs: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + feature_tokens = set(_ordered_unique([node_id, link_id])) + for ref in code_refs: + feature_tokens.update(_ordered_unique([ref.get("node_id"), ref.get("link_id")])) + refs: List[Dict[str, Any]] = [] + seen: set[str] = set() + for edge in edges: + edge_tokens = _ordered_unique([ + edge.get("rpg_node_id"), + edge.get("source_node_id"), + edge.get("target_node_id"), + edge.get("source_link_id"), + edge.get("target_link_id"), + ]) + if not feature_tokens.intersection(edge_tokens): + continue + for side in ("source", "target"): + leaf = endpoint_leaf(edge, side) + if not leaf: + continue + leaf_id = str(leaf.get("id") or "") + if not leaf_id or leaf_id in feature_tokens or leaf_id in seen: + continue + seen.add(leaf_id) + refs.append(leaf) + return refs + + def endpoint_group(parent: Dict[str, Any], owner_id: str, name: str, kind: str) -> Dict[str, Any]: + return _hierarchy_child(parent, _node_link_id(kind, owner_id), name, kind) + + for node in semantic_nodes: + node_id = str(node.get("node_id") or "") + link_id = str(node.get("link_id") or _node_link_id("rpg", node_id)) + feature_name = str(node.get("name") or node.get("symbol") or node_id or "feature") + group_parts = _semantic_hierarchy_parts(node) + code_refs = code_refs_for(node) + parent = root + trail: List[str] = [] + for part in group_parts: + trail.append(part) + group_path = " / ".join(trail) + group_node = semantic_node_by_path.get(group_path) + if group_node: + group_node_id = str(group_node.get("node_id") or "") + group = _hierarchy_child(parent, str(group_node.get("link_id") or _node_link_id("rpg", group_node_id)), part, semantic_tree_kind(group_node)) + merge_semantic_metadata(group, group_node, part, group_path) + else: + group = _hierarchy_child(parent, _node_link_id("feature-path", "/".join(trail)), part, "feature_group") + group["feature_name"] = part + group["feature_path"] = group_path + merge_code_metadata(group, code_refs) + parent = group + leaf_feature_path = feature_path_text(node, group_parts, feature_name) + leaf: Dict[str, Any] = { + "id": link_id, + "node_id": node_id, + "name": feature_name, + "feature_name": feature_name, + "feature_path": leaf_feature_path, + "kind": semantic_tree_kind(node), + "state": node.get("state"), + "mapping_status": node.get("mapping_status"), + "aliases": _ordered_unique([node_id, link_id, _node_link_id("feature-path", leaf_feature_path)]), + } + for key in ( + "type", + "node_type", + "path", + "breadcrumb", + "breadcrumb_path", + "locate_status", + "score", + "reason", + "apply_action", + "changed_files", + "hidden_counts", + "warning_types", + "source", + ): + _set_if_present(leaf, key, node.get(key)) + merge_code_metadata(leaf, code_refs) + context_group: Optional[Dict[str, Any]] = None + for ref in relation_endpoint_refs(node_id, link_id, code_refs): + if context_group is None: + context_group = endpoint_group(leaf, link_id, "Relation endpoints", "context_group") + append_endpoint(context_group, ref) + append_or_merge_hierarchy_leaf(parent, leaf) + + root_context_group: Optional[Dict[str, Any]] = None + for edge in edges: + for side in ("source", "target"): + leaf = endpoint_leaf(edge, side) + if not leaf or str(leaf.get("id") or "") in attached_endpoint_ids: + continue + if root_context_group is None: + root_context_group = endpoint_group(root, "unassigned", "Additional relation endpoints", "context_group") + append_endpoint(root_context_group, leaf) + return root + + +def _focused_graph_default_focus( + semantic_nodes: List[Dict[str, Any]], + code_nodes: List[Dict[str, Any]], + mappings: List[Dict[str, Any]], + edges: List[Dict[str, Any]], + warnings: List[Dict[str, Any]], +) -> Dict[str, Any]: + semantic_links = [str(node.get("link_id") or _node_link_id("rpg", node.get("node_id"))) for node in semantic_nodes] + focused_semantic_links = [ + str(node.get("link_id") or _node_link_id("rpg", node.get("node_id"))) + for node in semantic_nodes + if node.get("changed") + or node.get("changed_files") + or node.get("diff_anchor") + or node.get("change") + or node.get("apply_action") + ] + code_links = [str(node.get("link_id") or _node_link_id("code", node.get("node_id") or node.get("dep_node_id"))) for node in code_nodes] + semantic_link_set = set(semantic_links) + code_to_feature_links: Dict[str, List[str]] = {} + hierarchy_paths_by_link: Dict[str, List[str]] = {} + + def remember_code_feature(code_link: Any, feature_link: Any) -> None: + if code_link in (None, "") or feature_link in (None, ""): + return + code_text = str(code_link) + feature_text = str(feature_link) + values = code_to_feature_links.setdefault(code_text, []) + if feature_text not in values: + values.append(feature_text) + + def remember_path(alias: Any, path_ids: List[str]) -> None: + if alias in (None, "") or not path_ids: + return + alias_text = str(alias) + if alias_text not in hierarchy_paths_by_link: + hierarchy_paths_by_link[alias_text] = path_ids + + rpg_link_by_node_id = { + str(node.get("node_id")): str(node.get("link_id") or _node_link_id("rpg", node.get("node_id"))) + for node in semantic_nodes + if node.get("node_id") not in (None, "") + } + semantic_link_by_path: Dict[str, str] = {} + for node in semantic_nodes: + link_id = str(node.get("link_id") or _node_link_id("rpg", node.get("node_id"))) + feature_name = str(node.get("name") or node.get("symbol") or node.get("node_id") or "") + full_parts: List[str] = [] + for key in ("breadcrumb_path", "feature_path"): + full_parts = _hierarchy_segments(node.get(key)) + if full_parts: + break + if not full_parts: + full_parts = _semantic_hierarchy_parts(node) + ([feature_name] if feature_name else []) + if full_parts: + semantic_link_by_path.setdefault(" / ".join(full_parts), link_id) + + semantic_path_ids_by_link: Dict[str, List[str]] = {} + for node in semantic_nodes: + link_id = str(node.get("link_id") or _node_link_id("rpg", node.get("node_id"))) + trail: List[str] = [] + path_ids: List[str] = ["focused-graph-root"] + for part in _semantic_hierarchy_parts(node): + trail.append(part) + group_path = " / ".join(trail) + path_ids.append(semantic_link_by_path.get(group_path) or _node_link_id("feature-path", "/".join(trail))) + path_ids.append(link_id) + semantic_path_ids_by_link[link_id] = path_ids + remember_path(link_id, path_ids) + remember_path(node.get("node_id"), path_ids) + + code_link_by_node_id = { + str(node.get("node_id") or node.get("dep_node_id")): str(node.get("link_id") or _node_link_id("code", node.get("node_id") or node.get("dep_node_id"))) + for node in code_nodes + if (node.get("node_id") or node.get("dep_node_id")) not in (None, "") + } + + for mapping in mappings: + source = mapping.get("source_link_id") or rpg_link_by_node_id.get(str(mapping.get("rpg_node_id") or mapping.get("node_id") or "")) + target_node_id = mapping.get("code_node_id") or mapping.get("dep_node_id") + target = mapping.get("target_link_id") or code_link_by_node_id.get(str(target_node_id or "")) or _node_link_id("code", target_node_id) + remember_code_feature(target, source) + remember_code_feature(target_node_id, source) + for node in code_nodes: + node_id = node.get("node_id") or node.get("dep_node_id") + code_link = str(node.get("link_id") or _node_link_id("code", node_id)) + for rpg_link in _listify(node.get("mapped_rpg_link_ids")): + remember_code_feature(code_link, rpg_link) + remember_code_feature(node_id, rpg_link) + + for code_id, code_link in code_link_by_node_id.items(): + feature_links = code_to_feature_links.get(code_link) or code_to_feature_links.get(code_id) or [] + if feature_links: + for feature_link in feature_links: + base_path = semantic_path_ids_by_link.get(str(feature_link)) + if not base_path: + continue + code_path = base_path + [_node_link_id("code_group", feature_link), code_link] + remember_path(code_link, code_path) + remember_path(code_id, code_path) + else: + code_path = ["focused-graph-root", _node_link_id("code_group", "unassigned"), code_link] + remember_path(code_link, code_path) + remember_path(code_id, code_path) + + endpoint_link_ids: List[Any] = [] + context_link_ids: List[Any] = [] + + def endpoint_link(edge: Dict[str, Any], side: str) -> str: + node_id = edge.get(f"{side}_node_id") + node_text = str(node_id or "") + return str( + edge.get(f"{side}_link_id") + or rpg_link_by_node_id.get(node_text) + or code_link_by_node_id.get(node_text) + or _node_link_id("context", node_text) + ) + + def owner_feature_links(edge: Dict[str, Any]) -> List[str]: + owners: List[Any] = [] + owners.append(rpg_link_by_node_id.get(str(edge.get("rpg_node_id") or ""))) + for side in ("source", "target"): + node_id = edge.get(f"{side}_node_id") + link_id = endpoint_link(edge, side) + if str(node_id or "") in rpg_link_by_node_id: + owners.append(rpg_link_by_node_id[str(node_id)]) + owners.extend(code_to_feature_links.get(str(node_id or ""), [])) + owners.extend(code_to_feature_links.get(link_id, [])) + return _ordered_unique(owners) + + for edge in edges: + owners = owner_feature_links(edge) + for side in ("source", "target"): + link_id = endpoint_link(edge, side) + node_id = edge.get(f"{side}_node_id") + endpoint_link_ids.append(link_id) + endpoint_link_ids.append(node_id) + if link_id in semantic_link_set: + continue + if link_id in code_links: + continue + context_link_ids.append(link_id) + context_path_base = None + for feature_link in owners: + base_path = semantic_path_ids_by_link.get(str(feature_link)) + if base_path: + context_path_base = base_path + [_node_link_id("context_group", feature_link)] + break + if context_path_base is None: + context_path_base = ["focused-graph-root", _node_link_id("context_group", "unassigned")] + context_path = context_path_base + [link_id] + remember_path(link_id, context_path) + remember_path(node_id, context_path) + + changed_feature_links: List[Any] = [] + changed_code_links: List[str] = [] + for node in code_nodes: + link_id = str(node.get("link_id") or _node_link_id("code", node.get("node_id") or node.get("dep_node_id"))) + if node.get("changed") or node.get("changed_files") or node.get("diff_anchor"): + changed_code_links.append(link_id) + changed_feature_links.extend(_listify(node.get("mapped_rpg_link_ids")) + code_to_feature_links.get(link_id, [])) + node_link_ids = _ordered_unique(focused_semantic_links + changed_feature_links + context_link_ids) + + expanded_node_ids: List[Any] = ["focused-graph-root"] + focused_path_node_ids: List[Any] = [] + focused_tree_node_ids: List[Any] = [] + + def focus_tree_link(link_id: Any) -> None: + link_text = str(link_id or "") + if not link_text: + return + path_ids = hierarchy_paths_by_link.get(link_text) + if path_ids: + expanded_node_ids.extend(path_ids[:-1]) + focused_path_node_ids.extend(path_ids) + focused_tree_node_ids.append(path_ids[-1]) + return + feature_links = [link_text] if link_text in semantic_link_set else code_to_feature_links.get(link_text, []) + if not feature_links: + feature_links = [link_text] if link_text.startswith("feature-path-") or link_text == "focused-graph-root" else [] + for feature_link in feature_links: + feature_path = semantic_path_ids_by_link.get(feature_link, [feature_link]) + expanded_node_ids.extend(feature_path[:-1]) + focused_path_node_ids.extend(feature_path) + focused_tree_node_ids.append(feature_path[-1]) + + for link_id in node_link_ids: + focus_tree_link(link_id) + + expanded_node_ids = _ordered_unique(expanded_node_ids) + focused_path_node_ids = _ordered_unique(focused_path_node_ids) + focused_tree_node_ids = _ordered_unique(focused_tree_node_ids) + focused_code_link_ids = _ordered_unique([link_id for link_id in node_link_ids if link_id in code_links]) + return { + "node_link_ids": node_link_ids, + "focused_node_ids": node_link_ids, + "focused_tree_node_ids": focused_tree_node_ids, + "focused_code_link_ids": focused_code_link_ids, + "expanded_node_ids": expanded_node_ids, + "default_expanded_node_ids": expanded_node_ids, + "focused_path_node_ids": focused_path_node_ids, + "semantic_node_ids": _ordered_unique([node.get("node_id") for node in semantic_nodes]), + "code_node_ids": _ordered_unique([node.get("node_id") or node.get("dep_node_id") for node in code_nodes]), + "mapping_link_ids": _ordered_unique([mapping.get("link_id") for mapping in mappings]), + "edge_link_ids": _ordered_unique([edge.get("link_id") for edge in edges]), + "relation_endpoint_link_ids": _ordered_unique(endpoint_link_ids), + "context_node_ids": _ordered_unique(context_link_ids), + "edge_depth": 1, + "show_edges": True, + } + + +def _focused_graph_metadata( + semantic_nodes: List[Dict[str, Any]], + code_nodes: List[Dict[str, Any]], + mappings: List[Dict[str, Any]], + edges: List[Dict[str, Any]], + hidden_counts: Dict[str, Any], + warnings: List[Dict[str, Any]], + graph_context: Optional[Dict[str, Any]], +) -> Dict[str, Any]: + hierarchy = _focused_graph_hierarchy(semantic_nodes, code_nodes, mappings, edges, hidden_counts, warnings) + default_focus = _focused_graph_default_focus(semantic_nodes, code_nodes, mappings, edges, warnings) + focused_graph: Dict[str, Any] = { + "schema": "cmind.focused_graph.v1", + "hierarchy": hierarchy, + "default_focus": default_focus, + } + if graph_context: + focused_graph["graph_context"] = graph_context + if hidden_counts: + focused_graph["hidden_counts"] = hidden_counts + if warnings: + focused_graph["warning_count"] = len(warnings) + return focused_graph + + +def _build_nodes_view( + primary_rpg_nodes: List[Dict[str, Any]], + primary_code_nodes: List[Dict[str, Any]], + mappings: List[Dict[str, Any]], + edges: List[Dict[str, Any]], + hidden_counts: Dict[str, Any], + warnings: List[Dict[str, Any]], + changed_files: List[str], + diff_anchors: Dict[str, str], + *, + graph_context: Optional[Dict[str, Any]] = None, + current_rpg_nodes: Optional[Mapping[str, Dict[str, Any]]] = None, +) -> Dict[str, Any]: + selected_rpg_ids = {str(row.get("node_id")) for row in primary_rpg_nodes if row.get("node_id") not in (None, "")} + all_rpg_by_id: Dict[str, Dict[str, Any]] = {} + for node_id, node in (current_rpg_nodes or {}).items(): + node_id_text = str(node_id) + row = dict(node) + row["node_id"] = node_id_text + row.setdefault("link_id", _node_link_id("rpg", node_id_text)) + all_rpg_by_id[node_id_text] = row + for node in primary_rpg_nodes: + node_id = str(node.get("node_id") or "") + if not node_id: + continue + all_rpg_by_id[node_id] = {**all_rpg_by_id.get(node_id, {}), **node, "node_id": node_id} + rpg_by_id = all_rpg_by_id or {str(row.get("node_id")): row for row in primary_rpg_nodes if row.get("node_id") not in (None, "")} + code_by_id = {str(row.get("node_id") or row.get("dep_node_id")): row for row in primary_code_nodes if (row.get("node_id") or row.get("dep_node_id")) not in (None, "")} + code_ids_by_rpg: Dict[str, List[str]] = {} + warnings_by_rpg: Dict[str, List[Dict[str, Any]]] = {} + warnings_by_code: Dict[str, List[Dict[str, Any]]] = {} + for mapping in mappings: + rpg_id = mapping.get("rpg_node_id") or mapping.get("node_id") + code_id = mapping.get("code_node_id") or mapping.get("dep_node_id") + if rpg_id not in (None, "") and code_id not in (None, ""): + code_ids_by_rpg.setdefault(str(rpg_id), []).append(str(code_id)) + for warning in warnings: + if not isinstance(warning, dict): + continue + rpg_id = warning.get("node_id") or warning.get("rpg_node_id") + code_id = warning.get("dep_node_id") or warning.get("code_node_id") + if rpg_id not in (None, ""): + warnings_by_rpg.setdefault(str(rpg_id), []).append(warning) + if code_id not in (None, ""): + warnings_by_code.setdefault(str(code_id), []).append(warning) + + semantic_nodes: List[Dict[str, Any]] = [] + semantic_source_nodes = primary_rpg_nodes + for node in semantic_source_nodes: + node_id = str(node.get("node_id") or "") + is_selected = node_id in selected_rpg_ids + row: Dict[str, Any] = { + "node_id": node_id, + "link_id": node.get("link_id") or _node_link_id("rpg", node_id), + "state": _node_state(node) if is_selected else _node_state(node, "context"), + "mapping_status": node.get("mapping_status") or node.get("status"), + } + if is_selected: + row["selected"] = True + for key in ("name", "symbol", "type", "node_type", "path", "feature_path", "breadcrumb", "breadcrumb_path", "locate_status", "score", "reason", "apply_action"): + _set_if_present(row, key, node.get(key)) + changed_refs = _changed_file_refs(_listify(node.get("changed_files")) or _listify(node.get("affected_files")), diff_anchors) + if changed_refs: + row["changed_files"] = changed_refs + if isinstance(node.get("hidden_counts"), dict): + row["hidden_counts"] = node.get("hidden_counts") + mapped_code_ids = _ordered_unique(code_ids_by_rpg.get(node_id, [])) + if mapped_code_ids: + mapped_code_refs: List[Dict[str, Any]] = [] + for code_id in mapped_code_ids: + code = code_by_id.get(code_id, {}) + path = code.get("path") or code.get("file") or code.get("module") or _dep_node_path(code_id) + symbol = code.get("symbol") or code.get("name") or _symbol_from_dep(code_id, code) + ref: Dict[str, Any] = { + "node_id": code_id, + "link_id": _edge_endpoint_link_id(code_id, rpg_by_id, code_by_id), + "path": path, + "symbol": symbol, + } + for key in ("type", "kind", "line_range", "state", "source"): + _set_if_present(ref, key, code.get(key)) + mapped_code_refs.append(ref) + row["mapped_code"] = mapped_code_refs + row["mapped_code_node_ids"] = mapped_code_ids + row["mapped_code_link_ids"] = [ref["link_id"] for ref in mapped_code_refs] + row["mapped_code_paths"] = _ordered_unique([ref.get("path") for ref in mapped_code_refs]) + row["mapped_code_symbols"] = _ordered_unique([ref.get("symbol") for ref in mapped_code_refs]) + if row["mapped_code_paths"]: + row["mapped_code_path"] = row["mapped_code_paths"][0] + if row["mapped_code_symbols"]: + row["mapped_code_symbol"] = row["mapped_code_symbols"][0] + row["mapped_code_count"] = len(mapped_code_refs) + if warnings_by_rpg.get(node_id): + row["warning_types"] = _ordered_unique([warning.get("type") for warning in warnings_by_rpg[node_id]]) + semantic_nodes.append(row) + + code_nodes: List[Dict[str, Any]] = [] + for node in primary_code_nodes: + code_id = str(node.get("node_id") or node.get("dep_node_id") or "") + path = node.get("path") or _dep_node_path(code_id) + row = { + "node_id": code_id, + "dep_node_id": code_id, + "link_id": node.get("link_id") or _node_link_id("code", code_id), + "state": _node_state(node, "mapped"), + } + for key in ("name", "symbol", "type", "kind", "path", "module", "file", "signature", "source", "source_feature", "source_features", "breadcrumb"): + _set_if_present(row, key, node.get(key)) + _set_if_present(row, "path", path) + line_range = node.get("line_range") if isinstance(node.get("line_range"), dict) else _line_range_from(node) + if line_range: + row["line_range"] = line_range + changed_refs = _changed_file_refs([path] if path in diff_anchors else [], diff_anchors) + if changed_refs: + row["changed"] = True + row["changed_files"] = changed_refs + row["diff_anchor"] = changed_refs[0].get("diff_anchor") + rpg_ids = _ordered_unique([mapping.get("rpg_node_id") for mapping in mappings if (mapping.get("code_node_id") or mapping.get("dep_node_id")) == code_id]) + if rpg_ids: + row["mapped_rpg_node_ids"] = rpg_ids + row["mapped_rpg_link_ids"] = [_edge_endpoint_link_id(rpg_id, rpg_by_id, code_by_id) for rpg_id in rpg_ids] + if warnings_by_code.get(code_id): + row["warning_types"] = _ordered_unique([warning.get("type") for warning in warnings_by_code[code_id]]) + code_nodes.append(row) + + mapping_rows: List[Dict[str, Any]] = [] + for mapping in mappings: + rpg_id = mapping.get("rpg_node_id") or mapping.get("node_id") + code_id = mapping.get("code_node_id") or mapping.get("dep_node_id") + row: Dict[str, Any] = { + "link_id": _node_link_id("map", f"{rpg_id}-{code_id or 'missing'}"), + "rpg_node_id": rpg_id, + "source_link_id": _edge_endpoint_link_id(rpg_id, rpg_by_id, code_by_id), + "status": mapping.get("status") or "mapped", + "state": _node_state(mapping, "mapped"), + } + if code_id not in (None, ""): + row["code_node_id"] = code_id + row["dep_node_id"] = code_id + row["target_link_id"] = _edge_endpoint_link_id(code_id, rpg_by_id, code_by_id) + else: + row["target_state"] = "missing_mapping" + for key in ("source", "path", "reason"): + _set_if_present(row, key, mapping.get(key)) + changed_refs = _changed_file_refs(_listify(mapping.get("changed_files")), diff_anchors) + if changed_refs: + row["changed_files"] = changed_refs + mapping_rows.append(row) + + edge_rows: List[Dict[str, Any]] = [] + for edge in edges: + source_id = edge.get("source_node_id") + target_id = edge.get("target_node_id") + relation = edge.get("relation") or "dependency" + row: Dict[str, Any] = { + "link_id": _node_link_id("edge", f"{source_id}-{relation}-{target_id}"), + "source_node_id": source_id, + "target_node_id": target_id, + "source_link_id": edge.get("source_link_id") or _edge_endpoint_link_id(source_id, rpg_by_id, code_by_id), + "target_link_id": edge.get("target_link_id") or _edge_endpoint_link_id(target_id, rpg_by_id, code_by_id), + "relation": relation, + "state": edge.get("status") or "visible", + } + for key in ("direction", "source", "source_graph", "edge_source", "relation_source", "path", "reason", "rpg_node_id", "neighbor_node_id", "name", "source_path", "target_path", "source_name", "target_name"): + _set_if_present(row, key, edge.get(key)) + edge_rows.append(row) + + warning_rows = [_warning_link_fields(warning, rpg_by_id, code_by_id) for warning in warnings if isinstance(warning, dict)] + focused_graph = _focused_graph_metadata( + semantic_nodes, + code_nodes, + mapping_rows, + edge_rows, + hidden_counts, + warning_rows, + graph_context or {}, + ) + return { + "summary": { + "semantic_nodes": len(semantic_nodes), + "code_nodes": len(code_nodes), + "mappings": len(mapping_rows), + "edges": len(edge_rows), + "warnings": len(warning_rows), + "changed_files": len(changed_files), + }, + "semantic_nodes": semantic_nodes, + "code_nodes": code_nodes, + "mappings": mapping_rows, + "edges": edge_rows, + "hidden_counts": hidden_counts, + "warnings": warning_rows, + "changed_files": _changed_file_refs(changed_files, diff_anchors), + "hierarchy": focused_graph["hierarchy"], + "default_focus": focused_graph["default_focus"], + "focused_graph": focused_graph, + "caps": dict(_LEGACY_FOCUSED_VIEW_CAPS), + "graph_context": graph_context or {}, + } + + +_LEGACY_FOCUSED_VIEW_CAPS = {"primary_rpg_nodes": 20, "primary_code_nodes": 50, "edges": 80} +_FOCUSED_WARNING_TYPES = {"missing_mapping", "missing_reason", "stale_graph"} +_HIDDEN_CONTEXT_REASON = "selected by plan/impact, hidden because no modified mapped code" + + +def _set_if_present(row: Dict[str, Any], key: str, value: Any) -> None: + if value not in (None, ""): + row[key] = value + + +def _focus_reason(candidate: Dict[str, Any], impact: Dict[str, Any]) -> str: + for source in (candidate, impact): + for key in ("reason", "hit_reason", "rationale", "explanation"): + value = source.get(key) if isinstance(source, dict) else None + if value not in (None, ""): + return str(value) + return "" + + +def _slug_id(prefix: str, value: Any) -> str: + raw = str(value or prefix) + slug = re.sub(r"[^A-Za-z0-9_.-]+", "-", raw).strip(".-_") + return f"{prefix}-{slug or 'node'}" + + +def _line_range_from(row: Dict[str, Any]) -> Dict[str, Any]: + start = row.get("line_start") or row.get("start_line") or row.get("lineno") or row.get("line") + end = row.get("line_end") or row.get("end_line") or start + line_range: Dict[str, Any] = {} + _set_if_present(line_range, "start", start) + _set_if_present(line_range, "end", end) + return line_range + + +def _symbol_from_dep(dep_id: str, row: Dict[str, Any]) -> str: + for key in ("symbol", "name", "qualname", "qualified_name"): + value = row.get(key) + if value not in (None, ""): + return str(value) + dep_text = str(dep_id) + return dep_text.rsplit(":", 1)[-1] if ":" in dep_text else dep_text + + +def _rpg_node_entry(node_id: str, raw: Dict[str, Any]) -> Dict[str, Any]: + meta = raw.get("meta") if isinstance(raw.get("meta"), dict) else {} + row: Dict[str, Any] = {"node_id": node_id, "link_id": _slug_id("rpg", node_id)} + name = raw.get("name") or raw.get("title") + node_type = raw.get("node_type") or raw.get("type") or raw.get("type_name") + path = raw.get("path") or raw.get("file") or meta.get("path") + feature_path = raw.get("feature_path") or meta.get("feature_path") or raw.get("breadcrumb") or meta.get("breadcrumb") + _set_if_present(row, "name", name) + _set_if_present(row, "symbol", raw.get("symbol") or name) + _set_if_present(row, "node_type", node_type) + _set_if_present(row, "type", node_type) + _set_if_present(row, "path", path) + _set_if_present(row, "feature_path", feature_path) + _set_if_present(row, "breadcrumb", feature_path or path) + return row + + +def _dep_node_entry(dep_id: str, raw: Dict[str, Any]) -> Dict[str, Any]: + row: Dict[str, Any] = {"node_id": dep_id, "dep_node_id": dep_id, "link_id": _slug_id("code", dep_id)} + for key in ( + "name", + "symbol", + "qualname", + "qualified_name", + "type", + "kind", + "module", + "path", + "file", + "signature", + "line_start", + "line_end", + "start_line", + "end_line", + "lineno", + "line", + ): + _set_if_present(row, key, raw.get(key)) + if not row.get("path"): + _set_if_present(row, "path", row.get("file") or _dep_node_path(dep_id) or row.get("module")) + row["symbol"] = _symbol_from_dep(dep_id, row) + _set_if_present(row, "breadcrumb", row.get("path")) + line_range = _line_range_from(row) + if line_range: + row["line_range"] = line_range + return row + + +_CONTAINMENT_RELATIONS = {"contains", "CONTAINS", "composes", "COMPOSES"} + + +def _edge_relation(edge: Dict[str, Any], default: str) -> str: + attrs = edge.get("attrs") if isinstance(edge.get("attrs"), dict) else {} + relation = ( + edge.get("relation") + or edge.get("type") + or edge.get("edge_type") + or edge.get("kind") + or attrs.get("relation") + or attrs.get("type") + or attrs.get("edge_type") + or attrs.get("kind") + or default + ) + return str(relation) + + +def _coerce_rpg_edge(edge: Any) -> Optional[Dict[str, Any]]: + if isinstance(edge, dict): + source = edge.get("source_node_id") or edge.get("source") or edge.get("from") or edge.get("src") + target = edge.get("target_node_id") or edge.get("target") or edge.get("to") or edge.get("dst") + relation = _edge_relation(edge, "semantic") + if relation in _CONTAINMENT_RELATIONS or source in (None, "") or target in (None, ""): + return None + row = { + "source_node_id": str(source), + "target_node_id": str(target), + "relation": relation, + "source": "rpg_semantic", + "source_graph": "rpg", + "relation_source": "rpg_semantic", + "edge_source": "rpg", + } + for key in ("direction", "name", "path", "reason", "status"): + _set_if_present(row, key, edge.get(key)) + return row + if isinstance(edge, (list, tuple)) and len(edge) >= 2: + relation = str(edge[2]) if len(edge) >= 3 else "semantic" + if relation in _CONTAINMENT_RELATIONS: + return None + return {"source_node_id": str(edge[0]), "target_node_id": str(edge[1]), "relation": relation, "source": "rpg_semantic", "source_graph": "rpg", "relation_source": "rpg_semantic", "edge_source": "rpg"} + return None + + +def _coerce_dep_edge(edge: Any) -> Optional[Dict[str, Any]]: + if isinstance(edge, dict): + source = edge.get("source_node_id") or edge.get("source") or edge.get("from") or edge.get("caller") or edge.get("src") + target = edge.get("target_node_id") or edge.get("target") or edge.get("to") or edge.get("callee") or edge.get("dst") + relation = _edge_relation(edge, "dep_graph") + if relation in _CONTAINMENT_RELATIONS or source in (None, "") or target in (None, ""): + return None + row = { + "source_node_id": str(source), + "target_node_id": str(target), + "relation": relation, + "source": "dep_graph", + "source_graph": "dep_graph", + "relation_source": "dep_graph", + "edge_source": "dep_graph", + } + _set_if_present(row, "reason", edge.get("reason")) + return row + if isinstance(edge, (list, tuple)) and len(edge) >= 2: + relation = str(edge[2]) if len(edge) >= 3 else "dep_graph" + if relation in _CONTAINMENT_RELATIONS: + return None + return {"source_node_id": str(edge[0]), "target_node_id": str(edge[1]), "relation": relation, "source": "dep_graph", "source_graph": "dep_graph", "relation_source": "dep_graph", "edge_source": "dep_graph"} + return None + + +def _current_rpg_context() -> Tuple[ + Dict[str, Dict[str, Any]], + Dict[str, Dict[str, Any]], + List[Dict[str, Any]], + List[Dict[str, Any]], + Dict[str, List[str]], + Dict[str, List[str]], + List[Dict[str, Any]], +]: + rpg_data = _load_json_artifact(REPO_RPG_FILE) + if not isinstance(rpg_data, dict) or rpg_data.get("_error"): + return {}, {}, [], [], {}, {}, [{"type": "stale_graph", "message": f"RPG file not available: {REPO_RPG_FILE}"}] + + rpg_nodes: Dict[str, Dict[str, Any]] = {} + + def visit_rpg_node(raw: Any, breadcrumb: List[str]) -> None: + if not isinstance(raw, dict): + return + name = raw.get("name") or raw.get("title") + node_breadcrumb = breadcrumb + ([str(name)] if name not in (None, "") else []) + node_id = raw.get("id") or raw.get("node_id") + if node_id not in (None, ""): + row = _rpg_node_entry(str(node_id), raw) + if node_breadcrumb: + row["breadcrumb"] = node_breadcrumb + row["breadcrumb_path"] = " / ".join(node_breadcrumb) + rpg_nodes[str(node_id)] = row + children = raw.get("children") or [] + if isinstance(children, dict): + children = list(children.values()) + if isinstance(children, (list, tuple)): + for child in children: + visit_rpg_node(child, node_breadcrumb) + + visit_rpg_node(rpg_data.get("root"), []) + raw_rpg_nodes = rpg_data.get("nodes") + if isinstance(raw_rpg_nodes, dict): + for node_id, raw in raw_rpg_nodes.items(): + raw_dict = raw if isinstance(raw, dict) else {} + existing = rpg_nodes.get(str(node_id), {}) + rpg_nodes[str(node_id)] = {**_rpg_node_entry(str(node_id), raw_dict), **existing} + elif isinstance(raw_rpg_nodes, (list, tuple)): + for raw in raw_rpg_nodes: + if isinstance(raw, dict): + node_id = raw.get("id") or raw.get("node_id") + if node_id not in (None, ""): + existing = rpg_nodes.get(str(node_id), {}) + rpg_nodes[str(node_id)] = {**_rpg_node_entry(str(node_id), raw), **existing} + + dep_graph = rpg_data.get("dep_graph") if isinstance(rpg_data.get("dep_graph"), dict) else {} + dep_nodes: Dict[str, Dict[str, Any]] = {} + dep_to_rpg: Dict[str, List[str]] = {} + rpg_to_dep: Dict[str, List[str]] = {} + raw_dep_nodes = dep_graph.get("nodes") + if isinstance(raw_dep_nodes, dict): + dep_items = raw_dep_nodes.items() + elif isinstance(raw_dep_nodes, (list, tuple)): + dep_items = [] + for raw in raw_dep_nodes: + if isinstance(raw, dict): + dep_id = raw.get("id") or raw.get("node_id") or raw.get("dep_node_id") + if dep_id not in (None, ""): + dep_items.append((dep_id, raw)) + else: + dep_items = [] + for dep_id, raw in dep_items: + dep_id_text = str(dep_id) + raw_dict = raw if isinstance(raw, dict) else {} + dep_nodes[dep_id_text] = _dep_node_entry(dep_id_text, raw_dict) + linked = _ordered_unique([str(item) for item in _listify(raw_dict.get("rpg_nodes") or raw_dict.get("features") or raw_dict.get("source_features"))]) + if linked: + dep_to_rpg[dep_id_text] = linked + for rpg_id in linked: + rpg_to_dep.setdefault(rpg_id, []).append(dep_id_text) + + raw_dep_to_rpg = rpg_data.get("_dep_to_rpg_map") if isinstance(rpg_data.get("_dep_to_rpg_map"), dict) else {} + for dep_id, rpg_ids in raw_dep_to_rpg.items(): + dep_id_text = str(dep_id) + linked = _ordered_unique([str(item) for item in _listify(rpg_ids)]) + if linked: + dep_to_rpg[dep_id_text] = _ordered_unique((dep_to_rpg.get(dep_id_text) or []) + linked) + for rpg_id in linked: + rpg_to_dep.setdefault(rpg_id, []).append(dep_id_text) + + raw_rpg_to_dep = rpg_data.get("_rpg_to_dep_map") if isinstance(rpg_data.get("_rpg_to_dep_map"), dict) else {} + for rpg_id, dep_ids in raw_rpg_to_dep.items(): + rpg_id_text = str(rpg_id) + linked = _ordered_unique([str(item) for item in _listify(dep_ids)]) + if linked: + rpg_to_dep[rpg_id_text] = _ordered_unique((rpg_to_dep.get(rpg_id_text) or []) + linked) + for dep_id in linked: + dep_to_rpg.setdefault(dep_id, []).append(rpg_id_text) + + for rpg_id, dep_ids in list(rpg_to_dep.items()): + rpg_to_dep[rpg_id] = _ordered_unique(dep_ids) + for dep_id, rpg_ids in list(dep_to_rpg.items()): + dep_to_rpg[dep_id] = _ordered_unique(rpg_ids) + + def edge_values(value: Any) -> List[Any]: + if isinstance(value, dict): + return list(value.values()) + if isinstance(value, (list, tuple, set)): + return list(value) + return [] + + rpg_edges = [] + for raw_edge in edge_values(rpg_data.get("edges")) + edge_values(rpg_data.get("semantic_edges")) + edge_values(rpg_data.get("feature_edges")): + edge = _coerce_rpg_edge(raw_edge) + if edge: + rpg_edges.append(edge) + + dep_edges = [] + for raw_edge in edge_values(dep_graph.get("edges")) + edge_values(dep_graph.get("syntax_edges")): + edge = _coerce_dep_edge(raw_edge) + if edge: + dep_edges.append(edge) + + warnings: List[Dict[str, Any]] = [] + if not rpg_nodes and not dep_nodes: + warnings.append({"type": "stale_graph", "message": "Current RPG contains no indexed feature or dependency nodes"}) + return rpg_nodes, dep_nodes, rpg_edges, dep_edges, rpg_to_dep, dep_to_rpg, warnings + + +def _feature_evidence_groups( + artifacts: Dict[str, Any], + candidates: List[Dict[str, Any]], + code_deltas: List[Dict[str, Any]], + result: Dict[str, Any], +) -> Dict[str, Any]: + locate = artifacts.get("locate") if isinstance(artifacts.get("locate"), dict) else {} + plan = artifacts.get("plan") if isinstance(artifacts.get("plan"), dict) else {} + code_result = artifacts.get("code_result") if isinstance(artifacts.get("code_result"), dict) else {} + apply_result = artifacts.get("apply_result") if isinstance(artifacts.get("apply_result"), dict) else {} + impact_results = _impact_results(artifacts) + current_rpg_nodes, current_dep_nodes, current_rpg_edges, current_dep_edges, current_rpg_to_dep, current_dep_to_rpg, graph_warnings = _current_rpg_context() + current_graph_available = bool(current_rpg_nodes or current_dep_nodes) + locate_ids = {str(row.get("node_id")) for row in locate.get("results") or [] if isinstance(row, dict) and row.get("node_id")} + applied_by_id = { + str(row.get("node_id")): row + for row in apply_result.get("applied_features") or [] + if isinstance(row, dict) and row.get("node_id") + } + changed_files = _ordered_unique( + [_code_delta_file(delta) for delta in code_deltas] + + [str(path) for path in _listify(code_result.get("files_modified"))] + + [str(change.get("file_path")) for change in plan.get("code_changes") or [] if isinstance(change, dict) and change.get("file_path")] + ) + changed_ranges = changed_line_ranges_by_file(code_deltas) + warnings: List[Dict[str, Any]] = [] + warning_keys: set[str] = set() + + def add_warning(warning_type: str, message: str, **context: Any) -> None: + if warning_type not in _FOCUSED_WARNING_TYPES: + return + row: Dict[str, Any] = {"type": warning_type, "message": message} + for key, value in context.items(): + _set_if_present(row, key, value) + key = json.dumps(row, sort_keys=True, default=str) + if key not in warning_keys: + warning_keys.add(key) + warnings.append(row) + + for warning in graph_warnings: + if isinstance(warning, dict): + add_warning(str(warning.get("type") or "stale_graph"), str(warning.get("message") or "Current RPG graph may be stale")) + + if apply_result and apply_result.get("dep_graph_refreshed") is False: + add_warning("stale_graph", "Apply result says the dependency graph was not refreshed", apply_status=_apply_status(apply_result)) + + primary_rpg_nodes_all: List[Dict[str, Any]] = [] + mapping_rows_all: List[Dict[str, Any]] = [] + code_node_by_id: Dict[str, Dict[str, Any]] = {} + all_edges: List[Dict[str, Any]] = [] + impact_hidden_counts: Dict[str, int] = {} + + def count_value(value: Any, fallback: int) -> int: + try: + return int(value) + except (TypeError, ValueError): + return fallback + + def add_code_node(dep_id: Any, *, source_feature: Any = None, source: str = "mapping", fallback_path: Any = None) -> None: + if dep_id in (None, ""): + return + dep_id_text = str(dep_id) + if dep_id_text in code_node_by_id: + row = code_node_by_id[dep_id_text] + row["source"] = "+".join(_ordered_unique(_listify(row.get("source")) + [source])) + if source_feature not in (None, ""): + row["source_features"] = _ordered_unique(_listify(row.get("source_features")) + [str(source_feature)]) + return + current = current_dep_nodes.get(dep_id_text, {}) + row = dict(current) if current else {"node_id": dep_id_text, "dep_node_id": dep_id_text} + row.setdefault("node_id", dep_id_text) + row.setdefault("dep_node_id", dep_id_text) + row.setdefault("link_id", _node_link_id("code", dep_id_text)) + row.setdefault("symbol", _symbol_from_dep(dep_id_text, row)) + _set_if_present(row, "path", fallback_path) + row["status"] = "mapped" + row["graph_state"] = "available" if dep_id_text in current_dep_nodes else "unavailable" + row["source"] = source + if source_feature not in (None, ""): + row["source_feature"] = str(source_feature) + row["source_features"] = [str(source_feature)] + if row.get("path") in changed_files: + row["changed"] = True + if current_graph_available and dep_id_text not in current_dep_nodes: + row["status"] = "stale_graph" + row["graph_state"] = "stale_graph" + add_warning("stale_graph", "Mapped code node is absent from the current dependency graph", dep_node_id=dep_id_text) + code_node_by_id[dep_id_text] = row + + neighbor_specs = [ + ("callers", "caller", "upstream", "total_callers"), + ("callees", "callee", "downstream", "total_callees"), + ("imports", "imports", "downstream", "total_imports"), + ("inheritance", "inherits", "downstream", "total_inheritance"), + ] + edge_keys: set[tuple[str, str, str]] = set() + + def mapped_feature_id(dep_id: Any) -> Optional[str]: + dep_text = str(dep_id or "") + if not dep_text: + return None + mapped_ids = _ordered_unique(_listify(current_dep_to_rpg.get(dep_text)) + _listify(current_dep_nodes.get(dep_text, {}).get("rpg_nodes"))) + return str(mapped_ids[0]) if mapped_ids else None + + def neighbor_id_from(item: Any, impact_key: str) -> str: + if isinstance(item, dict): + side_keys = { + "callers": ("source_node_id", "caller_node_id", "caller", "from", "src"), + "callees": ("target_node_id", "callee_node_id", "callee", "to", "dst"), + "imports": ("target_node_id", "import_node_id", "imported_node_id", "import", "to", "dst"), + "inheritance": ("target_node_id", "parent_node_id", "base_node_id", "inherits", "to", "dst"), + } + for key in side_keys.get(impact_key, ()): + value = item.get(key) + if value not in (None, ""): + return str(value) + for key in ("rpg_node_id", "feature_node_id", "node_id", "dep_node_id", "code_node_id", "id", "path", "file"): + value = item.get(key) + if value not in (None, ""): + return str(value) + return "" + if item not in (None, ""): + return str(item) + return "" + + def neighbor_metadata(item: Any, neighbor_id: str) -> Dict[str, Any]: + if isinstance(item, dict): + row = dict(item) + else: + row = {} + dep = current_dep_nodes.get(neighbor_id, {}) + rpg = current_rpg_nodes.get(neighbor_id, {}) + path = row.get("path") or row.get("file") or dep.get("path") or dep.get("file") or dep.get("module") or rpg.get("path") + name = row.get("name") or row.get("symbol") or dep.get("name") or dep.get("symbol") or rpg.get("name") or rpg.get("symbol") + return {"path": path, "name": name} + + def add_impact_edges(node_id: str, impact: Dict[str, Any], focus_reason: str) -> None: + for impact_key, relation, direction, _total_key in neighbor_specs: + for item in _listify(impact.get(impact_key)): + raw_neighbor_id = neighbor_id_from(item, impact_key) + if not raw_neighbor_id: + continue + neighbor_id = mapped_feature_id(raw_neighbor_id) or raw_neighbor_id + if neighbor_id == node_id: + continue + if impact_key == "callers": + source_id, target_id = neighbor_id, node_id + else: + source_id, target_id = node_id, neighbor_id + key = (str(source_id), str(target_id), relation) + if key in edge_keys: + continue + edge_keys.add(key) + metadata = neighbor_metadata(item, raw_neighbor_id) + edge_row: Dict[str, Any] = { + "source_node_id": source_id, + "target_node_id": target_id, + "relation": relation, + "direction": direction, + "source": "impact", + "source_graph": "impact", + "edge_source": "impact", + "relation_source": impact_key, + "rpg_node_id": node_id, + "neighbor_node_id": neighbor_id, + "reason": focus_reason or f"impact {impact_key}", + } + if neighbor_id not in current_rpg_nodes: + side = "source" if impact_key == "callers" else "target" + edge_row[f"{side}_link_id"] = _node_link_id("context", raw_neighbor_id) + _set_if_present(edge_row, "path", metadata.get("path")) + _set_if_present(edge_row, "name", metadata.get("name")) + side = "source" if impact_key == "callers" else "target" + _set_if_present(edge_row, f"{side}_path", metadata.get("path")) + _set_if_present(edge_row, f"{side}_name", metadata.get("name")) + all_edges.append(edge_row) + + for candidate in candidates: + raw_node_id = candidate.get("node_id") + if raw_node_id in (None, ""): + continue + node_id = str(raw_node_id) + impact = impact_results.get(node_id) if isinstance(impact_results.get(node_id), dict) else {} + current_node = current_rpg_nodes.get(node_id, {}) + relations = _mapped_code_relations(candidate, impact) + relation_by_dep = {str(row.get("dep_node_id") or row.get("node_id")): row for row in relations if row.get("dep_node_id") or row.get("node_id")} + mapped_dep_ids = _ordered_unique(list(relation_by_dep) + (current_rpg_to_dep.get(node_id) or [])) + modified_dep_ids = _modified_dep_ids(mapped_dep_ids, relation_by_dep, current_dep_nodes, changed_ranges, changed_files) + relation_paths = _ordered_unique( + [row.get("path") for row in relations] + + [current_dep_nodes.get(dep_id, {}).get("path") for dep_id in mapped_dep_ids] + ) + affected_files = _ordered_unique(_listify(impact.get("affected_files")) + relation_paths) + relevant_files = set(_ordered_unique(affected_files + [ + candidate.get("path"), + candidate.get("meta_path"), + current_node.get("path"), + ])) + relevant_deltas = [delta for delta in code_deltas if _code_delta_file(delta) in relevant_files] + changed_for_node = _ordered_unique([_code_delta_file(delta) for delta in relevant_deltas]) + focus_reason = _focus_reason(candidate, impact) + if not focus_reason: + add_warning("missing_reason", "Focused view has no explicit selection reason", node_id=node_id) + if not mapped_dep_ids: + add_warning("missing_mapping", "Selected RPG node has no mapped code node", node_id=node_id) + for dep_id in mapped_dep_ids: + if current_graph_available and dep_id not in current_dep_nodes: + add_warning("stale_graph", "Mapped code node is absent from the current dependency graph", node_id=node_id, dep_node_id=dep_id) + if current_graph_available and node_id not in current_rpg_nodes: + add_warning("stale_graph", "Selected RPG node is absent from the current RPG graph", node_id=node_id) + + impact_summary = impact.get("impact_summary") if isinstance(impact.get("impact_summary"), dict) else {} + node_hidden_counts: Dict[str, int] = {} + for impact_key, _relation, _direction, total_key in neighbor_specs: + items = _listify(impact.get(impact_key)) + total = count_value(impact_summary.get(total_key), len(items)) + hidden = max(0, total - len(items)) + if hidden: + node_hidden_counts[impact_key] = hidden + impact_hidden_counts[impact_key] = impact_hidden_counts.get(impact_key, 0) + hidden + add_impact_edges(node_id, impact, focus_reason) + + apply_row = applied_by_id.get(node_id, {}) + rpg_row: Dict[str, Any] = dict(current_node) if current_node else {"node_id": node_id, "link_id": _node_link_id("rpg", node_id)} + rpg_row["node_id"] = node_id + rpg_row.setdefault("link_id", _node_link_id("rpg", node_id)) + rpg_row["status"] = "mapped" if mapped_dep_ids else "missing" + rpg_row["mapping_status"] = "mapped" if mapped_dep_ids else "missing" + rpg_row["graph_state"] = "available" if node_id in current_rpg_nodes else "unavailable" + if current_graph_available and node_id not in current_rpg_nodes: + rpg_row["graph_state"] = "stale_graph" + _set_if_present(rpg_row, "name", candidate.get("name") or impact.get("name") or current_node.get("name")) + _set_if_present(rpg_row, "symbol", candidate.get("symbol") or impact.get("symbol") or rpg_row.get("name")) + _set_if_present(rpg_row, "node_type", candidate.get("node_type") or candidate.get("type_name") or candidate.get("type") or current_node.get("node_type")) + _set_if_present(rpg_row, "type", rpg_row.get("node_type")) + _set_if_present(rpg_row, "path", candidate.get("path") or candidate.get("meta_path") or current_node.get("path")) + _set_if_present(rpg_row, "feature_path", candidate.get("feature_path") or current_node.get("feature_path")) + _set_if_present(rpg_row, "score", candidate.get("score")) + _set_if_present(rpg_row, "reason", focus_reason) + if node_id not in locate_ids and locate: + rpg_row["locate_status"] = candidate.get("locate_state") or "missing" + if affected_files: + rpg_row["affected_files"] = affected_files + if changed_for_node: + rpg_row["changed_files"] = changed_for_node + if node_hidden_counts: + rpg_row["hidden_counts"] = node_hidden_counts + _set_if_present(rpg_row, "apply_action", apply_row.get("action") or apply_row.get("change")) + primary_rpg_nodes_all.append(rpg_row) + + if mapped_dep_ids: + for dep_id in modified_dep_ids: + relation = relation_by_dep.get(dep_id, {}) + current_dep = current_dep_nodes.get(dep_id, {}) + path = current_dep.get("path") or relation.get("path") + source_parts = _ordered_unique(_listify(relation.get("source")) + (["current_rpg"] if dep_id in (current_rpg_to_dep.get(node_id) or []) else [])) + mapping_status = "mapped" + if current_graph_available and dep_id not in current_dep_nodes: + mapping_status = "stale_graph" + add_warning("stale_graph", "Mapped code node is absent from the current dependency graph", node_id=node_id, dep_node_id=dep_id) + mapping_row: Dict[str, Any] = { + "rpg_node_id": node_id, + "code_node_id": dep_id, + "dep_node_id": dep_id, + "status": mapping_status, + "source": "+".join(source_parts) or "selected_feature", + } + _set_if_present(mapping_row, "path", path) + _set_if_present(mapping_row, "reason", focus_reason) + if changed_for_node: + mapping_row["changed_files"] = changed_for_node + mapping_rows_all.append(mapping_row) + add_code_node(dep_id, source_feature=node_id, source=mapping_row["source"], fallback_path=path) + else: + mapping_row = {"rpg_node_id": node_id, "status": "missing"} + _set_if_present(mapping_row, "reason", focus_reason) + if changed_for_node: + mapping_row["changed_files"] = changed_for_node + mapping_rows_all.append(mapping_row) + + selected_rpg_ids_all = {str(row.get("node_id") or "") for row in primary_rpg_nodes_all if row.get("node_id") not in (None, "")} + modified_mapped_rpg_ids = _ordered_unique([row.get("rpg_node_id") for row in mapping_rows_all if row.get("code_node_id")]) + edge_endpoint_rpg_ids = _ordered_unique([ + endpoint + for edge in all_edges + for endpoint in (edge.get("source_node_id"), edge.get("target_node_id")) + if str(endpoint or "") in current_rpg_nodes or str(endpoint or "") in selected_rpg_ids_all + ]) + visible_rpg_ids = _ordered_unique(modified_mapped_rpg_ids + edge_endpoint_rpg_ids) + visible_rpg_id_set = set(visible_rpg_ids) + primary_rpg_by_id = { + str(row.get("node_id")): row + for row in primary_rpg_nodes_all + if row.get("node_id") not in (None, "") + } + primary_rpg_nodes: List[Dict[str, Any]] = [] + for node_id in visible_rpg_ids: + row = primary_rpg_by_id.get(node_id) + if row is None and node_id in current_rpg_nodes: + row = dict(current_rpg_nodes[node_id]) + row["node_id"] = node_id + row.setdefault("link_id", _node_link_id("rpg", node_id)) + row.setdefault("status", "context") + row.setdefault("graph_state", "available") + if row is not None: + primary_rpg_nodes.append(row) + visible_primary_rpg_ids = {str(row.get("node_id") or "") for row in primary_rpg_nodes} + hidden_context_nodes: List[Dict[str, Any]] = [] + for row in primary_rpg_nodes_all: + node_id = str(row.get("node_id") or "") + if not node_id or node_id in visible_primary_rpg_ids: + continue + hidden_row = dict(row) + hidden_row["hidden_reason"] = _HIDDEN_CONTEXT_REASON + hidden_row["reason"] = _HIDDEN_CONTEXT_REASON + hidden_context_nodes.append(hidden_row) + primary_code_nodes_all = list(code_node_by_id.values()) + visible_code_ids = { + str(row.get("code_node_id") or row.get("dep_node_id") or "") + for row in mapping_rows_all + if str(row.get("rpg_node_id") or "") in visible_primary_rpg_ids and (row.get("code_node_id") or row.get("dep_node_id")) not in (None, "") + } + primary_code_nodes = [row for row in primary_code_nodes_all if str(row.get("node_id") or row.get("dep_node_id") or "") in visible_code_ids] + mappings = [ + row + for row in mapping_rows_all + if str(row.get("rpg_node_id") or "") in visible_primary_rpg_ids + ] + edges = all_edges + hidden_counts: Dict[str, Any] = {} + for key, count in impact_hidden_counts.items(): + if count: + hidden_counts[key] = hidden_counts.get(key, 0) + count + + matched_files = {file_path for node in primary_rpg_nodes_all for file_path in node.get("changed_files") or []} + unmatched_code_deltas = [delta for delta in code_deltas if _code_delta_file(delta) not in matched_files] + mapped_code_relations = sum(1 for row in mappings if row.get("code_node_id")) + missing_mappings = sum(1 for row in mappings if row.get("status") == "missing") + diff_anchors = _diff_anchor_map(code_deltas) + graph_context = { + "current_graph_available": current_graph_available, + "current_rpg_nodes": len(current_rpg_nodes), + "current_dep_nodes": len(current_dep_nodes), + "current_rpg_edges": len(current_rpg_edges), + "current_dep_edges": len(current_dep_edges), + } + nodes_view = _build_nodes_view( + primary_rpg_nodes, + primary_code_nodes, + mappings, + edges, + hidden_counts, + warnings, + changed_files, + diff_anchors, + graph_context=graph_context, + current_rpg_nodes=current_rpg_nodes, + ) + summary = { + "selected_feature_groups": len(primary_rpg_nodes_all), + "primary_rpg_nodes": len(primary_rpg_nodes), + "primary_code_nodes": len(primary_code_nodes), + "mapped_code_relations": mapped_code_relations, + "missing_mappings": missing_mappings, + "edges": len(edges), + "warnings": len(warnings), + "changed_files": len(changed_files), + "hidden_context_nodes": len(hidden_context_nodes), + "review_status": result.get("type", "review"), + "apply_status": _apply_status(apply_result), + "verification_status": _test_status(result, code_result, apply_result), + } + nodes_view["hidden_context_nodes"] = hidden_context_nodes + nodes_summary = dict(nodes_view.get("summary", {})) + for key in ("selected_feature_groups", "mapped_code_relations", "missing_mappings", "hidden_context_nodes", "review_status", "apply_status", "verification_status"): + _set_if_present(nodes_summary, key, summary.get(key)) + nodes_view["summary"] = nodes_summary + return { + "summary": summary, + "nodes_view": nodes_view, + "primary_rpg_nodes": primary_rpg_nodes, + "primary_code_nodes": primary_code_nodes, + "mappings": mappings, + "edges": edges, + "hidden_counts": hidden_counts, + "hidden_context_nodes": hidden_context_nodes, + "warnings": warnings, + "changed_files": changed_files, + "unmatched_code_deltas": unmatched_code_deltas, + "apply": { + "status": _apply_status(apply_result), + "dep_graph_refreshed": apply_result.get("dep_graph_refreshed"), + }, + "review": { + "status": result.get("type", "review"), + "success": result.get("success", result.get("type") == "skipped"), + "iterations": len(result.get("iterations") or []), + "suggestions": len(result.get("suggestions") or []), + }, + } + + +def _review_summary_cards( + result: Dict[str, Any], + artifacts: Dict[str, Any], + focused_view: Optional[Dict[str, Any]] = None, +) -> List[Dict[str, Any]]: + plan = artifacts.get("plan") if isinstance(artifacts.get("plan"), dict) else {} + code_result = artifacts.get("code_result") if isinstance(artifacts.get("code_result"), dict) else {} + apply_result = artifacts.get("apply_result") if isinstance(artifacts.get("apply_result"), dict) else {} + summary = focused_view.get("summary") if isinstance(focused_view, dict) else {} + result_value = "passed" if result.get("success", result.get("type") == "skipped") else "failed" + changed_files = summary.get("changed_files", len(code_result.get("files_modified") or [])) + return [ + {"label": "Review", "value": result.get("type", "review"), "detail": result_value}, + {"label": "Selected features", "value": summary.get("selected_feature_groups", len(plan.get("affected_nodes") or []))}, + {"label": "Mapped code relations", "value": summary.get("mapped_code_relations", 0)}, + {"label": "Missing mappings", "value": summary.get("missing_mappings", 0)}, + {"label": "Changed files", "value": changed_files}, + {"label": "Verification", "value": summary.get("verification_status") or _test_status(result, code_result, apply_result)}, + ] + + +def _review_timeline(result: Dict[str, Any], artifacts: Dict[str, Any]) -> List[Dict[str, Any]]: + validate = artifacts.get("validate") if isinstance(artifacts.get("validate"), dict) else None + locate = artifacts.get("locate") if isinstance(artifacts.get("locate"), dict) else None + plan = artifacts.get("plan") if isinstance(artifacts.get("plan"), dict) else None + impact = artifacts.get("impact") if isinstance(artifacts.get("impact"), dict) else None + code_result = artifacts.get("code_result") if isinstance(artifacts.get("code_result"), dict) else None + apply_result = artifacts.get("apply_result") if isinstance(artifacts.get("apply_result"), dict) else None + apply_reason = "artifact not found" + if apply_result is not None: + apply_reason = ( + f"{len(apply_result.get('applied_features') or [])} applied features; " + f"dep_graph_refreshed={apply_result.get('dep_graph_refreshed')}" + ) + return [ + {"name": "validate", "status": validate.get("type") if validate else "missing", "reason": validate.get("message", "") if validate else "artifact not found"}, + {"name": "locate", "status": locate.get("type") if locate else "missing", "reason": f"{len(locate.get('results') or [])} candidates" if locate else "artifact not found"}, + {"name": "plan", "status": "available" if plan else "missing", "reason": f"{len(plan.get('code_changes') or [])} code changes" if plan else "artifact not found"}, + {"name": "impact", "status": impact.get("type", "available") if impact else "missing", "reason": f"{len((impact.get('results') or {}))} impact result sets" if impact else "artifact not found"}, + {"name": "code", "status": code_result.get("last_status") if code_result else "missing", "reason": code_result.get("last_error") or f"success={code_result.get('success')}" if code_result else "artifact not found"}, + {"name": "apply/dep-refresh", "status": _apply_status(apply_result or {}), "reason": apply_reason}, + {"name": "review", "status": result.get("type"), "reason": result.get("reason") or f"success={result.get('success')}"}, + ] + + +def _review_verification(result: Dict[str, Any], artifacts: Dict[str, Any]) -> List[Dict[str, Any]]: + checks: List[Dict[str, Any]] = [] + validate = artifacts.get("validate") if isinstance(artifacts.get("validate"), dict) else None + code_result = artifacts.get("code_result") if isinstance(artifacts.get("code_result"), dict) else None + apply_result = artifacts.get("apply_result") if isinstance(artifacts.get("apply_result"), dict) else None + if validate: + checks.append({"name": "validate", "status": validate.get("type"), "detail": validate.get("message", "")}) + if code_result: + checks.append({"name": "code", "status": code_result.get("success"), "detail": code_result.get("last_error") or code_result.get("last_status")}) + if apply_result is None: + checks.append({"name": "apply", "status": "missing", "detail": "artifact not found"}) + else: + checks.append({ + "name": "apply", + "status": _apply_status(apply_result), + "detail": f"{len(apply_result.get('applied_features') or [])} applied features", + }) + test_result = apply_result.get("test_result") if isinstance(apply_result, dict) and isinstance(apply_result.get("test_result"), dict) else {} + checks.append({ + "name": "test", + "status": _test_status(result, code_result or {}, apply_result or {}), + "detail": "apply test_result" if test_result else "review/code status fallback", + }) + checks.append({ + "name": "dep_graph refresh", + "status": apply_result.get("dep_graph_refreshed") if apply_result is not None else "missing", + "detail": f"apply status={_apply_status(apply_result or {})}" if apply_result is not None else "artifact not found", + }) + checks.append({"name": "review", "status": result.get("success", result.get("type") == "skipped"), "detail": result.get("reason") or result.get("type")}) + for iteration in result.get("iterations") or []: + checks.append({ + "name": f"review iteration {iteration.get('iteration')}", + "status": iteration.get("post_pytest_passed"), + "detail": iteration.get("agent_detail", ""), + }) + return checks + + +def _status_from_bool(value: Any) -> Optional[str]: + if value is True: + return "passed" + if value is False: + return "failed" + return None + + +def _apply_status(apply_result: Dict[str, Any]) -> Any: + return apply_result.get("type") or apply_result.get("status") or "missing" + + +def _test_status(result: Dict[str, Any], code_result: Dict[str, Any], apply_result: Dict[str, Any]) -> Any: + test_result = apply_result.get("test_result") if isinstance(apply_result.get("test_result"), dict) else {} + status = _status_from_bool(test_result.get("passed")) + if status: + return status + for iteration in reversed(result.get("iterations") or []): + if isinstance(iteration, dict): + status = _status_from_bool(iteration.get("post_pytest_passed")) + if status: + return status + if code_result.get("last_status"): + return code_result.get("last_status") + return _status_from_bool(result.get("success")) + + +def _rollback_path(apply_result: Dict[str, Any]) -> Any: + if apply_result.get("rollback_path"): + return apply_result.get("rollback_path") + backups = apply_result.get("backups") if isinstance(apply_result.get("backups"), dict) else {} + return backups.get("rpg") or backups.get("dep_graph") or apply_result.get("rollback_command") + + +def _artifact_path_pointers(artifact_rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + pointers: List[Dict[str, Any]] = [] + for row in artifact_rows: + pointer: Dict[str, Any] = {} + for key in ("label", "path", "status"): + if row.get(key) not in (None, ""): + pointer[key] = row.get(key) + if pointer: + pointers.append(pointer) + return pointers + + +def _compact_plan_audit(plan: Dict[str, Any]) -> Dict[str, Any]: + changes = [] + for change in plan.get("code_changes") or []: + if not isinstance(change, dict): + continue + row: Dict[str, Any] = {} + for key in ("file_path", "change_type", "action"): + if change.get(key) not in (None, ""): + row[key] = change.get(key) + if row: + changes.append(row) + return { + "affected_nodes": [str(node_id) for node_id in _listify(plan.get("affected_nodes"))], + "code_changes": changes, + } + + +def _compact_impact_audit(impact: Dict[str, Any]) -> Dict[str, Any]: + results = impact.get("results") if isinstance(impact.get("results"), dict) else {} + affected_files: List[Any] = [] + mapped_relations = 0 + node_summaries = [] + for node_id, row in results.items(): + row = row if isinstance(row, dict) else {} + dep_nodes = _listify(row.get("dep_nodes")) + files = _listify(row.get("affected_files")) + affected_files.extend(files) + mapped_relations += len(dep_nodes) + summary = row.get("impact_summary") if isinstance(row.get("impact_summary"), dict) else {} + node_summaries.append({ + "node_id": node_id, + "mapped_code_relations": len(dep_nodes), + "affected_files": len(files), + "total_callers": summary.get("total_callers", len(row.get("callers") or [])), + "total_callees": summary.get("total_callees", len(row.get("callees") or [])), + }) + return { + "type": impact.get("type"), + "result_count": len(results), + "affected_files": _ordered_unique(affected_files), + "mapped_code_relations": mapped_relations, + "results": node_summaries, + } + + +def _compact_code_audit(code_result: Dict[str, Any]) -> Dict[str, Any]: + return { + "success": code_result.get("success"), + "last_status": code_result.get("last_status"), + "commit_sha": code_result.get("commit_sha"), + "files_modified": [str(path) for path in _listify(code_result.get("files_modified"))], + "iterations": len(code_result.get("iterations") or []), + } + + +def _compact_apply_audit(apply_result: Dict[str, Any]) -> Dict[str, Any]: + applied = [] + for feature in apply_result.get("applied_features") or []: + if not isinstance(feature, dict): + continue + row: Dict[str, Any] = {} + for key in ("node_id", "action", "change"): + if feature.get(key) not in (None, ""): + row[key] = feature.get(key) + if row: + applied.append(row) + test_result = apply_result.get("test_result") if isinstance(apply_result.get("test_result"), dict) else {} + audit = { + "status": _apply_status(apply_result), + "dep_graph_refreshed": apply_result.get("dep_graph_refreshed"), + "applied_features": applied, + "rollback_path": _rollback_path(apply_result), + "test_status": _status_from_bool(test_result.get("passed")), + } + for key in ("backup_timestamp", "backups", "confirmed", "before_state", "rollback_command"): + if apply_result.get(key) not in (None, "", [], {}): + audit[key] = apply_result.get(key) + return audit + + +def _compact_review_audit(result: Dict[str, Any]) -> Dict[str, Any]: + return { + "status": result.get("type", "review"), + "success": result.get("success", result.get("type") == "skipped"), + "iterations": len(result.get("iterations") or []), + "suggestions": len(result.get("suggestions") or []), + "reason": result.get("reason"), + } + + +def _compact_review_evidence( + artifacts: Dict[str, Any], + artifact_rows: List[Dict[str, Any]], + result: Dict[str, Any], +) -> Dict[str, Any]: + validate = artifacts.get("validate") if isinstance(artifacts.get("validate"), dict) else {} + locate = artifacts.get("locate") if isinstance(artifacts.get("locate"), dict) else {} + plan = artifacts.get("plan") if isinstance(artifacts.get("plan"), dict) else {} + impact = artifacts.get("impact") if isinstance(artifacts.get("impact"), dict) else {} + code_result = artifacts.get("code_result") if isinstance(artifacts.get("code_result"), dict) else {} + apply_result = artifacts.get("apply_result") if isinstance(artifacts.get("apply_result"), dict) else {} + evidence = { + "artifact_paths": _artifact_path_pointers(artifact_rows), + "audit_summary": { + "validate": {"type": validate.get("type"), "message": validate.get("message")}, + "locate": { + "type": locate.get("type"), + "query": locate.get("query"), + "candidate_count": len(locate.get("results") or []), + }, + "plan": _compact_plan_audit(plan), + "impact": _compact_impact_audit(impact), + "code": _compact_code_audit(code_result), + "apply": _compact_apply_audit(apply_result), + "review": _compact_review_audit(result), + }, + } + for key in ("run_id", "parent_run_id", "is_final", "report_scope", "published_to"): + evidence[key] = result.get(key) + return evidence + + +def _user_decision(result: Dict[str, Any], artifacts: Dict[str, Any]) -> UserDecisionEvent: + apply_result = artifacts.get("apply_result") if isinstance(artifacts.get("apply_result"), dict) else {} + code_result = artifacts.get("code_result") if isinstance(artifacts.get("code_result"), dict) else {} + current_head = read_head(REPO_DIR) + before_state = apply_result.get("before_state") if apply_result.get("before_state") else current_head + branch = before_state.get("head_branch") if isinstance(before_state, dict) else None + if not branch and isinstance(current_head, dict): + branch = current_head.get("head_branch") + confirmed = apply_result.get("confirmed") if "confirmed" in apply_result else None + return UserDecisionEvent( + decision="apply", + branch=branch, + before_state=before_state, + rollback_path=_rollback_path(apply_result), + confirmed=confirmed, + apply_status=_apply_status(apply_result), + test_status=_test_status(result, code_result, apply_result), + ) + + +class _ReportPayload: + def __init__(self, run: CommandRun, focused_view: Dict[str, Any], report_dir: Optional[Path] = None): + self.run = run + self.focused_view = focused_view + self.report_dir = report_dir + + def to_dict(self) -> Dict[str, Any]: + data = self.run.to_dict() + if self.focused_view: + data["focused_view"] = self.focused_view + if self.report_dir is not None: + data["report_dir"] = str(self.report_dir) + return data + + +def _publish_review_report( + result: Dict[str, Any], + plan_path: Path, + impact_path: Optional[Path], + *, + report_scope: str = "final", + report_dir: Optional[Path] = None, + parent_run_id: Optional[str] = None, +) -> Dict[str, Any]: + report_scope = _normalize_report_scope(report_scope) + previous_result = _load_existing_review_result() + internal_report_paths = _existing_internal_report_paths(previous_result) + run_id = str(result.get("run_id") or uuid.uuid4().hex) + if parent_run_id is None: + parent_run_id = result.get("parent_run_id") + + result["run_id"] = run_id + result["parent_run_id"] = parent_run_id + result["is_final"] = report_scope == "final" + result["report_scope"] = report_scope + result["internal_report_paths"] = internal_report_paths + + if report_scope == "none": + result["published_to"] = None + result.pop("report_path", None) + _write_review_result(result) + return result + + target_report_dir = _report_target_dir(report_scope, report_dir) + report_timestamp = _report_timestamp() + result["published_to"] = str(_expected_report_path(target_report_dir, report_timestamp)) + _write_review_result(result) + + artifacts = _load_review_artifacts(plan_path, impact_path) + candidates = _selected_candidate_rows(artifacts) + code_deltas = _code_delta_rows(artifacts) + focused_view = _feature_evidence_groups(artifacts, candidates, code_deltas, result) + visible_rpg_ids = { + str(row.get("node_id")) + for row in focused_view.get("primary_rpg_nodes", []) + if isinstance(row, dict) and row.get("node_id") not in (None, "") + } + rpg_delta_rows = [row for row in candidates if str(row.get("node_id") or "") in visible_rpg_ids] if visible_rpg_ids else candidates + artifact_rows = _artifact_links(plan_path, impact_path, internal_report_paths) + evidence = _compact_review_evidence(artifacts, artifact_rows, result) + try: + report_run = CommandRun( + command="rpg_edit", + title="CoderMind rpg_edit Explain View", + status=str(result.get("type", "review")), + timestamp=report_timestamp, + summary=_review_summary_cards(result, artifacts, focused_view), + steps=[ + StepEvent(name=row.get("name", "stage"), status=row.get("status"), reason=row.get("reason", "")) + for row in _review_timeline(result, artifacts) + ], + rpg_deltas=[ + RPGDeltaEvent( + node_id=row.get("node_id"), + name=row.get("name"), + type=row.get("type"), + path=row.get("path") or row.get("meta_path"), + score=row.get("score"), + ) + for row in rpg_delta_rows + ], + dep_graph_deltas=[ + DepGraphDeltaEvent( + dep_node_id=row.get("node_id"), + path=row.get("path"), + source_feature=row.get("source_feature"), + change=row.get("change"), + ) + for row in _dep_node_rows(rpg_delta_rows) + ], + retrievals=[ + RetrievalEvent(query=row.get("query"), tool=row.get("tool"), hits=row.get("hits"), reason=row.get("reason")) + for row in _retrieval_rows(artifacts, candidates) + ], + artifacts=[ + ArtifactEvent(label=row["label"], path=row["path"], status=row.get("status")) + for row in artifact_rows + ], + code_deltas=[ + CodeDeltaEvent( + file=row.get("file"), + change_type=row.get("change_type"), + before=row.get("before"), + after=row.get("after"), + diff=row.get("diff"), + ) + for row in code_deltas + ], + verification=[ + VerificationEvent(name=row.get("name", "verification"), status=row.get("status"), detail=row.get("detail")) + for row in _review_verification(result, artifacts) + ], + user_decisions=[_user_decision(result, artifacts)], + evidence=evidence, + ) + report_path = write_command_report(_ReportPayload(report_run, focused_view, target_report_dir)) + result["report_path"] = str(report_path) + result["published_to"] = str(report_path) + if report_scope == "internal": + result["internal_report_paths"] = _ordered_unique(internal_report_paths + [str(report_path)]) + except Exception as exc: + result["report_error"] = str(exc) + _write_review_result(result) + return result + + # --------------------------------------------------------------------------- # Review prompt template # --------------------------------------------------------------------------- @@ -385,7 +2723,10 @@ def impact_review( impact_path: Optional[Path], repo_path: Path, max_iterations: int = 3, - timeout: int = 600, + timeout: int = 1200, + report_scope: str = "final", + report_dir: Optional[Path] = None, + parent_run_id: Optional[str] = None, ) -> Dict[str, Any]: """Run impact-scoped review with iterative repair.""" from run_batch import dispatch_sub_agent @@ -546,7 +2887,14 @@ def impact_review( all_suggestions.append(s) if all_suggestions: results["suggestions"] = all_suggestions - return results + return _publish_review_report( + results, + plan_path, + impact_path, + report_scope=report_scope, + report_dir=report_dir, + parent_run_id=parent_run_id, + ) # --------------------------------------------------------------------------- @@ -566,11 +2914,20 @@ def main(): help="Repository root path") parser.add_argument("--max-iterations", type=int, default=3, help="Maximum review+repair iterations (default: 3)") - parser.add_argument("--timeout", type=int, default=600, - help="Sub-agent timeout per iteration in seconds (default: 600)") + parser.add_argument("--timeout", type=int, default=1200, + help="Sub-agent timeout per iteration in seconds (default: 1200)") + parser.add_argument("--report-scope", choices=sorted(_REPORT_SCOPES), default="final", + help="HTML report publication scope (default: %(default)s)") + parser.add_argument("--no-report", action="store_true", + help="Persist review JSON without writing an HTML report") + parser.add_argument("--report-dir", type=Path, default=None, + help="Base report directory (default: .cmind/reports)") + parser.add_argument("--parent-run-id", default=None, + help="Parent command run ID to record in report evidence") parser.add_argument("--json", action="store_true", help="Output as JSON") args = parser.parse_args() + report_scope = "none" if args.no_report else args.report_scope logging.basicConfig( level=logging.INFO, @@ -583,6 +2940,14 @@ def main(): if not args.plan.exists(): result = {"type": "error", "message": f"Plan not found: {args.plan}"} + result = _publish_review_report( + result, + args.plan, + args.impact, + report_scope=report_scope, + report_dir=args.report_dir, + parent_run_id=args.parent_run_id, + ) print(json.dumps(result) if args.json else f"Error: {result['message']}") return 1 @@ -599,12 +2964,21 @@ def main(): if total_callers == 0 and affected_files <= 1: result = { "type": "skipped", + "success": True, "reason": f"Impact too small for sub-agent review " f"(callers={total_callers}, files={affected_files}). " f"Agent self-review is sufficient.", } + result = _publish_review_report( + result, + args.plan, + args.impact, + report_scope=report_scope, + report_dir=args.report_dir, + parent_run_id=args.parent_run_id, + ) print(json.dumps(result, indent=2) if args.json else - f"Skipped: {result['reason']}") + f"Skipped: {result['reason']}\nReport: {result.get('report_path', '')}") return 0 result = impact_review( @@ -613,12 +2987,21 @@ def main(): repo_path=repo_path, max_iterations=args.max_iterations, timeout=args.timeout, + report_scope=report_scope, + report_dir=args.report_dir, + parent_run_id=args.parent_run_id, ) - print(json.dumps(result, indent=2) if args.json else - f"Review {'PASSED' if result['success'] else 'FAILED'} " - f"({len(result['iterations'])} iterations, " - f"{result['total_duration']:.1f}s)") + if args.json: + print(json.dumps(result, indent=2)) + else: + print( + f"Review {'PASSED' if result['success'] else 'FAILED'} " + f"({len(result['iterations'])} iterations, " + f"{result['total_duration']:.1f}s)" + ) + if result.get("report_path"): + print(f"Report: {result['report_path']}") return 0 if result["success"] else 1 diff --git a/CoderMind/scripts/rpg_edit/validate.py b/CoderMind/scripts/rpg_edit/validate.py index e52f7b0..d098fee 100644 --- a/CoderMind/scripts/rpg_edit/validate.py +++ b/CoderMind/scripts/rpg_edit/validate.py @@ -12,7 +12,15 @@ if str(SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(SCRIPTS_DIR)) -from common.paths import REPO_RPG_FILE, DEP_GRAPH_FILE # noqa: E402 +from common.paths import REPO_RPG_FILE, DEP_GRAPH_FILE, RPG_EDIT_VALIDATE_FILE # noqa: E402 + + +def _write_validate_result(result: dict) -> None: + RPG_EDIT_VALIDATE_FILE.parent.mkdir(parents=True, exist_ok=True) + RPG_EDIT_VALIDATE_FILE.write_text( + json.dumps(result, indent=2, ensure_ascii=False), + encoding="utf-8", + ) def main(): @@ -31,6 +39,7 @@ def main(): if not args.rpg.exists(): result = {"type": "error", "error_code": "rpg_not_found", "message": f"RPG file not found: {args.rpg}"} + _write_validate_result(result) print(json.dumps(result) if args.json else f"Error: {result['message']}") return 1 @@ -40,6 +49,7 @@ def main(): except Exception as e: result = {"type": "error", "error_code": "rpg_load_failed", "message": f"Failed to load RPG: {e}"} + _write_validate_result(result) print(json.dumps(result) if args.json else f"Error: {result['message']}") return 1 @@ -52,6 +62,7 @@ def main(): "Run /cmind.encode to (re)build it; the embedded " "dep_graph rides inside rpg.json." )} + _write_validate_result(result) print(json.dumps(result) if args.json else f"Error: {result['message']}") return 1 @@ -64,6 +75,7 @@ def main(): "dep_to_rpg": len(svc.rpg._dep_to_rpg_map), "feature_to_dep": len(svc.rpg._feature_to_dep_map), } + _write_validate_result(result) print(json.dumps(result, indent=2) if args.json else f"Ready: {result['nodes']} nodes, dep_graph={'yes' if has_dep_graph else 'no'}") return 0 diff --git a/CoderMind/scripts/rpg_encoder/rpg_evolution.py b/CoderMind/scripts/rpg_encoder/rpg_evolution.py index 5b9507e..18c483b 100644 --- a/CoderMind/scripts/rpg_encoder/rpg_evolution.py +++ b/CoderMind/scripts/rpg_encoder/rpg_evolution.py @@ -758,6 +758,18 @@ def process_diff( save_path=dep_graph_save_path, ) + last_rpg._last_diff_summary = { + "added": 0, + "deleted": 0, + "modified": 0, + "renamed": 0, + } + last_rpg._last_diff_files = { + "added": [], + "deleted": [], + "modified": [], + "renamed": [], + } total_time = time.time() - global_start logger.info( "\nNo changes detected for [%s]. RPG remains unchanged.\n" @@ -795,6 +807,20 @@ def process_diff( save_path=dep_graph_save_path, ) + diff_summary = { + "added": len(add_files), + "deleted": len(deleted_files), + "modified": len(modified_result), + "renamed": 0, + } + ctx["last_rpg"]._last_diff_summary = diff_summary + ctx["last_rpg"]._last_diff_files = { + "added": add_files, + "deleted": deleted_files, + "modified": list(modified_result.keys()), + "renamed": [], + } + # Save results result = { "repo_name": repo_name, @@ -804,11 +830,7 @@ def process_diff( "structure": ctx["last_rpg"].to_dict(), "feature_tree": ctx["last_rpg"].get_functionality_graph(), }, - "diff_summary": { - "added": len(add_files), - "deleted": len(deleted_files), - "modified": len(modified_result), - }, + "diff_summary": diff_summary, } if save_path: diff --git a/CoderMind/scripts/rpg_encoder/run_encode.py b/CoderMind/scripts/rpg_encoder/run_encode.py index d9ed2a0..6c299a1 100644 --- a/CoderMind/scripts/rpg_encoder/run_encode.py +++ b/CoderMind/scripts/rpg_encoder/run_encode.py @@ -27,9 +27,53 @@ from common.paths import RPG_FILE, RPG_HTML_FILE, WORKSPACE_ROOT, ensure_cmind_dir # noqa: E402 from common.rpg_io import atomic_write_rpg # noqa: E402 +from common.run_events import ArtifactEvent, CommandRun, StepEvent, VerificationEvent # noqa: E402 +from common.run_report import write_command_report # noqa: E402 from common.trajectory import Trajectory # noqa: E402 +def _attach_encode_report(result: dict) -> dict: + try: + dep_summary = [] + if result.get("dep_nodes") is not None: + dep_summary.append(f"nodes={result.get('dep_nodes')}") + if result.get("dep_edges") is not None: + dep_summary.append(f"edges={result.get('dep_edges')}") + if result.get("dep_to_rpg_map_size") is not None: + dep_summary.append(f"mapped={result.get('dep_to_rpg_map_size')}") + report_path = write_command_report(CommandRun( + command="encode", + title="CoderMind encode Explain View", + status=result.get("status"), + summary=[ + {"label": "repo", "value": result.get("repo_name", "unknown")}, + {"label": "RPG nodes", "value": result.get("node_count", 0)}, + {"label": "RPG edges", "value": result.get("edge_count", 0)}, + {"label": "dep graph", "value": ", ".join(dep_summary) or "not recorded"}, + {"label": "output", "value": result.get("output_path", "")}, + {"label": "visualization", "value": result.get("viz_path", result.get("viz_error", ""))}, + {"label": "trajectory", "value": result.get("trajectory", "")}, + ], + steps=[ + StepEvent(name="parse_rpg", status="recorded", reason=f"nodes={result.get('node_count', 0)}, edges={result.get('edge_count', 0)}"), + StepEvent(name="dep_graph", status="recorded" if dep_summary else "not recorded", reason=", ".join(dep_summary)), + StepEvent(name="save_rpg", status="recorded" if result.get("output_path") else "not recorded", reason=result.get("output_path", "")), + StepEvent(name="visualize", status="recorded" if result.get("viz_path") else "not recorded", reason=result.get("viz_path") or result.get("viz_error", "")), + ], + artifacts=[ + ArtifactEvent(label="rpg_json", path=result.get("output_path")), + ArtifactEvent(label="rpg_html", path=result.get("viz_path")), + ArtifactEvent(label="trajectory", path=result.get("trajectory")), + ], + verification=[VerificationEvent(name="encode", status=result.get("status"))], + evidence=result, + )) + result["report_path"] = str(report_path) + except Exception as exc: + result["report_error"] = str(exc) + return result + + def run_encode( repo_dir: str | None = None, repo_name: str | None = None, @@ -54,7 +98,12 @@ def run_encode( repo_dir = os.path.abspath(repo_dir) if not os.path.isdir(repo_dir): - return {"status": "error", "error": f"Repository directory not found: {repo_dir}"} + return _attach_encode_report({ + "status": "error", + "error": f"Repository directory not found: {repo_dir}", + "repo_name": repo_name or os.path.basename(repo_dir) or "unknown", + "output_path": output or str(RPG_FILE), + }) if repo_name is None: repo_name = os.path.basename(repo_dir) or "unknown" @@ -188,12 +237,18 @@ def run_encode( traj.complete(stats) stats["trajectory"] = str(traj.trajectory_file) - return {"status": "success", **stats} + return _attach_encode_report({"status": "success", **stats}) except Exception as exc: logger.exception("Encoding failed: %s", exc) traj.fail(str(exc)) - return {"status": "error", "error": str(exc), "trajectory": str(traj.trajectory_file)} + return _attach_encode_report({ + "status": "error", + "error": str(exc), + "repo_name": repo_name, + "output_path": output, + "trajectory": str(traj.trajectory_file), + }) def main(): diff --git a/CoderMind/scripts/rpg_encoder/run_update_rpg.py b/CoderMind/scripts/rpg_encoder/run_update_rpg.py index c368ef4..d62f7e0 100644 --- a/CoderMind/scripts/rpg_encoder/run_update_rpg.py +++ b/CoderMind/scripts/rpg_encoder/run_update_rpg.py @@ -228,8 +228,7 @@ def run_update_rpg( # to a full rebuild (rebase / diverged path). meta_git_advanced = False try: - ws_root = WORKSPACE_ROOT - current = read_head(ws_root) + current = read_head(cur_repo_dir) if current: updated_rpg.set_git_meta( head_commit=current["head_commit"], @@ -254,6 +253,8 @@ def run_update_rpg( post_nodes = len(updated_rpg.nodes) post_edges = _serialized_feature_edges(result_data) post_dep_stats = _serialized_dep_stats(result_data) + diff_summary = getattr(updated_rpg, "_last_diff_summary", None) + diff_files = getattr(updated_rpg, "_last_diff_files", None) stats = { "repo_name": repo_name, @@ -275,6 +276,10 @@ def run_update_rpg( "previous_commit": pre_commit, "new_commit": (updated_rpg.git_meta or {}).get("head_commit"), } + if isinstance(diff_summary, dict): + stats["diff_summary"] = diff_summary + if isinstance(diff_files, dict): + stats["diff_files"] = diff_files try: stats["functional_areas"] = len(updated_rpg.get_functional_areas()) except Exception: diff --git a/CoderMind/scripts/rpg_visualize.py b/CoderMind/scripts/rpg_visualize.py index 9f1b8de..d5849a7 100644 --- a/CoderMind/scripts/rpg_visualize.py +++ b/CoderMind/scripts/rpg_visualize.py @@ -15,12 +15,39 @@ """ import argparse +import html import json import sys from pathlib import Path from typing import Any, Dict, List +_D3_ASSET = Path(__file__).resolve().parent / "common" / "assets" / "d3.v7.min.js" + + +def _html_escape(value: Any) -> str: + return html.escape(str(value), quote=True) + + +def _json_for_script(value: Any) -> str: + data = json.dumps(value, ensure_ascii=False) + return ( + data.replace("&", "\\u0026") + .replace("<", "\\u003c") + .replace(">", "\\u003e") + .replace("
", "\\u2028") + .replace("
", "\\u2029") + ) + + +def _inline_d3() -> str: + try: + source = _D3_ASSET.read_text(encoding="utf-8").strip() + except OSError: + return "" + return source.replace(" dict: with open(path, "r", encoding="utf-8") as f: return json.load(f) @@ -227,13 +254,197 @@ def to_tree(nid): "children": [to_tree(r) for r in sorted(roots)]} +def _id_set(values) -> set: + if values is None: + return set() + if isinstance(values, (str, bytes)): + values = [values] + return {str(value) for value in values if value not in (None, "")} + + +def _collect_tree_ids(node: dict) -> set: + ids = set() + node_id = node.get("id") + if node_id not in (None, ""): + ids.add(str(node_id)) + for child in node.get("children", []) or []: + ids.update(_collect_tree_ids(child)) + return ids + + +def _filter_tree(node: dict, keep_ids: set, *, keep_root: bool = True) -> dict | None: + children = [ + child + for child in (_filter_tree(child, keep_ids, keep_root=False) for child in node.get("children", []) or []) + if child is not None + ] + node_id = str(node.get("id", "")) + if keep_root or node_id in keep_ids or children: + filtered = dict(node) + filtered["children"] = children + return filtered + return None + + +def _expand_rpg_focus(data: dict, rpg_ids: set, dep_ids: set, include_neighbors: bool) -> set: + focus = set(rpg_ids) + dep_to_rpg = data.get("_dep_to_rpg_map", {}) if isinstance(data.get("_dep_to_rpg_map"), dict) else {} + for dep_id, mapped_rpg_ids in dep_to_rpg.items(): + mapped = _id_set(mapped_rpg_ids) + if dep_id in dep_ids: + focus.update(mapped) + if focus.intersection(mapped): + dep_ids.add(str(dep_id)) + + if include_neighbors: + for edge in get_semantic_edges(data): + src = str(edge.get("src", "")) + dst = str(edge.get("dst", "")) + if src in focus or dst in focus: + focus.update([src, dst]) + return focus + + +def _dep_parent_map(dep_graph: dict) -> Dict[str, str]: + parent: Dict[str, str] = {} + for edge in dep_graph.get("edges", []) or []: + if edge.get("attrs", {}).get("type", "") in ("contains", "CONTAINS"): + parent[str(edge.get("dst", ""))] = str(edge.get("src", "")) + return {child: par for child, par in parent.items() if child and par} + + +def _expand_dep_focus(data: dict, rpg_ids: set, dep_ids: set, include_neighbors: bool) -> set: + dep_graph = data.get("dep_graph", {}) if isinstance(data.get("dep_graph"), dict) else {} + raw_nodes = dep_graph.get("nodes", {}) if isinstance(dep_graph.get("nodes"), dict) else {} + focus = set(dep_ids) + dep_to_rpg = data.get("_dep_to_rpg_map", {}) if isinstance(data.get("_dep_to_rpg_map"), dict) else {} + for dep_id, mapped_rpg_ids in dep_to_rpg.items(): + if rpg_ids.intersection(_id_set(mapped_rpg_ids)): + focus.add(str(dep_id)) + for dep_id, attrs in raw_nodes.items(): + if not isinstance(attrs, dict): + continue + mapped = _id_set(attrs.get("rpg_nodes")) + if mapped.intersection(rpg_ids): + focus.add(str(dep_id)) + if str(dep_id) in focus: + rpg_ids.update(mapped) + + if include_neighbors: + for edge in dep_graph.get("edges", []) or []: + edge_type = edge.get("attrs", {}).get("type", "") + if edge_type in ("contains", "CONTAINS"): + continue + src = str(edge.get("src", "")) + dst = str(edge.get("dst", "")) + if src in focus or dst in focus: + focus.update([src, dst]) + + parent = _dep_parent_map(dep_graph) + for dep_id in list(focus): + cur = dep_id + while cur in parent: + cur = parent[cur] + focus.add(cur) + for dep_id in list(focus): + attrs = raw_nodes.get(dep_id) + if isinstance(attrs, dict): + rpg_ids.update(_id_set(attrs.get("rpg_nodes"))) + rpg_ids.update(_id_set(dep_to_rpg.get(dep_id))) + return focus + + +def build_focused_graph_data( + data: dict, + *, + rpg_node_ids: List[str] | None = None, + dep_node_ids: List[str] | None = None, + include_neighbors: bool = True, +) -> dict: + selected_rpg = _id_set(rpg_node_ids) + selected_dep = _id_set(dep_node_ids) + if not selected_rpg and not selected_dep: + return dict(data) + + rpg_focus = _expand_rpg_focus(data, set(selected_rpg), set(selected_dep), include_neighbors) + dep_focus = _expand_dep_focus(data, rpg_focus, set(selected_dep), include_neighbors) + rpg_focus = _expand_rpg_focus(data, rpg_focus, dep_focus, include_neighbors=False) + + tree = normalize_to_tree(data) + filtered_tree = _filter_tree(tree, rpg_focus) or {"id": "__root__", "name": "Focused graph", "children": []} + tree_ids = _collect_tree_ids(filtered_tree) + matched_rpg = sorted(selected_rpg.intersection(tree_ids)) + + semantic_edges = [ + edge for edge in get_semantic_edges(data) + if str(edge.get("src", "")) in tree_ids and str(edge.get("dst", "")) in tree_ids + ] + + dep_graph = data.get("dep_graph", {}) if isinstance(data.get("dep_graph"), dict) else {} + raw_nodes = dep_graph.get("nodes", {}) if isinstance(dep_graph.get("nodes"), dict) else {} + raw_edges = dep_graph.get("edges", []) if isinstance(dep_graph.get("edges"), list) else [] + filtered_dep_nodes = {dep_id: attrs for dep_id, attrs in raw_nodes.items() if str(dep_id) in dep_focus} + filtered_dep_edges = [ + edge for edge in raw_edges + if str(edge.get("src", "")) in dep_focus and str(edge.get("dst", "")) in dep_focus + ] + dep_ids = {str(dep_id) for dep_id in filtered_dep_nodes} + matched_dep = sorted(selected_dep.intersection(dep_ids)) + + dep_to_rpg = data.get("_dep_to_rpg_map", {}) if isinstance(data.get("_dep_to_rpg_map"), dict) else {} + filtered_map = {} + for dep_id, mapped_rpg_ids in dep_to_rpg.items(): + if str(dep_id) not in dep_ids: + continue + mapped = [str(rpg_id) for rpg_id in mapped_rpg_ids if str(rpg_id) in tree_ids] + if mapped: + filtered_map[str(dep_id)] = mapped + + focused = dict(data) + if isinstance(data.get("nodes"), list): + focused["nodes"] = [ + dict(node) for node in data["nodes"] + if isinstance(node, dict) and str(node.get("id", "")) in tree_ids + ] + focused["root"] = filtered_tree + focused["edges"] = semantic_edges + focused["dep_graph"] = {**dep_graph, "nodes": filtered_dep_nodes, "edges": filtered_dep_edges} + focused["_dep_to_rpg_map"] = filtered_map + focused["_focused_graph"] = { + "selected_rpg_nodes": sorted(selected_rpg), + "selected_dep_nodes": sorted(selected_dep), + "matched_rpg_nodes": matched_rpg, + "matched_dep_nodes": matched_dep, + "rpg_node_count": len(tree_ids), + "dep_node_count": len(dep_ids), + "semantic_edge_count": len(semantic_edges), + "dep_edge_count": len(filtered_dep_edges), + } + return focused + + +def generate_focused_html( + data: dict, + *, + rpg_node_ids: List[str] | None = None, + dep_node_ids: List[str] | None = None, + include_neighbors: bool = True, +) -> str: + return generate_html(build_focused_graph_data( + data, + rpg_node_ids=rpg_node_ids, + dep_node_ids=dep_node_ids, + include_neighbors=include_neighbors, + )) + + def generate_html(data: dict) -> str: tree = normalize_to_tree(data) semantic_edges = get_semantic_edges(data) dep = extract_dep_graph(data) dep_tree = build_dep_tree(data) dep_to_rpg = data.get("_dep_to_rpg_map", {}) - repo_name = data.get("repo_name", "Unknown") + repo_name_html = _html_escape(data.get("repo_name", "Unknown")) feat_node_count = count_nodes(tree) feat_edge_count = len(semantic_edges) @@ -242,32 +453,35 @@ def generate_html(data: dict) -> str: for e in semantic_edges: r = e.get("relation", "unknown") edge_types[r] = edge_types.get(r, 0) + 1 - feat_edge_summary = ", ".join(f"{k}: {v}" for k, v in sorted(edge_types.items())) + feat_edge_summary = _html_escape(", ".join(f"{k}: {v}" for k, v in sorted(edge_types.items()))) # Dep stats dep_node_count = len(dep["nodes"]) dep_edge_count = len(dep["edges"]) - dep_edge_summary = ", ".join(f"{k}: {v}" for k, v in sorted(dep["stats"].items())) + dep_edge_summary = _html_escape(", ".join(f"{k}: {v}" for k, v in sorted(dep["stats"].items()))) has_dep = dep_node_count > 0 map_count = sum(len(v) for v in dep_to_rpg.values()) has_map = len(dep_to_rpg) > 0 - tree_json = json.dumps(tree) - edges_json = json.dumps(semantic_edges) - dep_nodes_json = json.dumps(dep["nodes"]) - dep_edges_json = json.dumps(dep["edges"]) - dep_parent_json = json.dumps(dep["parent_map"]) - dep_tree_json = json.dumps(dep_tree) - dep_to_rpg_json = json.dumps(dep_to_rpg) + tree_json = _json_for_script(tree) + edges_json = _json_for_script(semantic_edges) + dep_nodes_json = _json_for_script(dep["nodes"]) + dep_edges_json = _json_for_script(dep["edges"]) + dep_parent_json = _json_for_script(dep["parent_map"]) + dep_tree_json = _json_for_script(dep_tree) + dep_to_rpg_json = _json_for_script(dep_to_rpg) + d3_js = _inline_d3() return f""" -RPG: {repo_name} - +RPG: {repo_name_html} +