@wbern/obscene
Advanced tools
+51
-45
| #!/usr/bin/env node | ||
| import{execSync as Pt,spawnSync as ze}from"child_process";import{existsSync as z,realpathSync as Ve,writeFileSync as Wt}from"fs";import{relative as jt}from"path";import{Command as Bt,InvalidArgumentError as Ke}from"commander";import{execSync as D,spawnSync as Y}from"child_process";import{existsSync as Ze,mkdtempSync as et,readFileSync as he,rmSync as de}from"fs";import{tmpdir as tt}from"os";import{join as Q}from"path";var nt=[".obsignore",".obsceneignore"];function ye(){for(let e of nt)try{return he(e,"utf-8").split(` | ||
| `).map(n=>n.trim()).filter(n=>n!==""&&!n.startsWith("#"))}catch(t){if(t&&typeof t=="object"&&"code"in t&&t.code==="ENOENT")continue;throw t}return[]}var Z=[{title:"Test files and test infrastructure",patterns:[{pattern:"*.test.*",comment:"Unit test files"},{pattern:"*.spec.*",comment:"Spec test files"},{pattern:"*.integration.test.*",comment:"Integration tests"},{pattern:"test-setup.*",comment:"Test setup files"},{pattern:"test-utils.*",comment:"Test utility files"},{pattern:"test-helpers.*",comment:"Test helper files"},{pattern:"__tests__/**",comment:"Test directories"},{pattern:"__mocks__/**",comment:"Mock directories"},{pattern:"*.stories.*",comment:"Storybook stories"},{pattern:"*.d.ts",comment:"TypeScript declaration files"}]},{title:"Lock files and package manifests",patterns:[{pattern:"package.json",comment:"npm package manifest"},{pattern:"package-lock.json",comment:"npm lock file"},{pattern:"pnpm-lock.yaml",comment:"pnpm lock file"},{pattern:"yarn.lock",comment:"Yarn lock file"},{pattern:"bun.lock",comment:"Bun lock file"}]}],ot=.5,it=.8,J=5,K=3,H={complexity:{weak:3,plausible:10,acceptable:30},nesting:{weak:3,plausible:10,acceptable:30},defects:{weak:5,plausible:15,acceptable:50},authors:{weak:2,plausible:4,acceptable:8},coupling:{weak:5,plausible:30,acceptable:100}},T={complexity:"Engineering judgment: any rank ordering needs \u2265 3 items to be meaningful; higher tiers scale from there. No paper prescribes these exact cutoffs.",nesting:"Engineering judgment, informed by Campbell (SonarSource 2018) Cognitive Complexity which assigns a compounding penalty per nesting level. The 3/10/30 sample-count tiers are not from the paper.",defects:"code-maat's --min-revs default of 5 (Adam Tornhill); higher tiers are engineering judgment. Gall et al. (IWPSE 2003) and Hassan (ICSE 2009) study co-change and change-entropy but do not prescribe a specific commit-count floor.",authors:"Engineering judgment. Bird et al. (FSE 2011) Don't Touch My Code! shows minor contributors (< 5% of commits) correlate with elevated defects, motivating attention to contributor count \u2014 but the 2/4/8 tiers here are not from the paper.",coupling:"code-maat defaults (--min-revs 5, --max-changeset-size 30, Adam Tornhill). CodeScene's documented temporal-coupling default filters files with fewer than 10 commits. The 30/100 upper tiers are engineering judgment.",composite:"Reciprocal Rank Fusion (Cormack et al., SIGIR 2009) fuses multiple independent rankings; min-of-inputs is a strict monotone aggregator \u2014 when every input ranking is at confidence level L, the composite cannot exceed L."};function A(e,t,n,o,i){let s;return t<n.weak?s="inconclusive":t<n.plausible?s="weak":t<n.acceptable?s="plausible":s="acceptable",{level:s,reason:i(s),inputs:{metric:e,value:t,thresholds:n},source:o}}function we(e,t){return t.some(n=>n.test(e))}function Ce(e){let t=e.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,"\u27E8GLOBSTAR\u27E9").replace(/\*/g,"[^/]*").replace(/⟨GLOBSTAR⟩/g,".*").replace(/\?/g,".");return new RegExp(t)}function M(e){let t=e.replaceAll("\\","/");return t.startsWith("./")?t.slice(2):t}function N(e=[],t){let n=e.map(Ce),o;try{o=D("scc --by-file --format json --no-cocomo --no-gen",{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:t})}catch(r){throw r&&typeof r=="object"&&"code"in r&&r.code==="ENOENT"?new Error("scc not found. Install it: https://github.com/boyter/scc#install"):r}let i=JSON.parse(o.toString()),s=[];for(let r of i)for(let l of r.Files){let u=M(l.Location);we(u,n)||s.push({file:u,code:l.Code,lines:l.Lines,complexity:l.Complexity,comments:l.Comment,complexityDensity:l.Code>0?Math.round(l.Complexity/l.Code*100)/100:0})}return s.sort((r,l)=>l.complexity-r.complexity)}function be(e,t,n){let o;try{o=D(e,{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:n})}catch{throw new Error(t)}let i=new Map;for(let s of o.toString().split(` | ||
| `)){let r=M(s.trim());r&&i.set(r,(i.get(r)??0)+1)}return i}function W(e,t){return be(`git log --since="${e} months ago" --format="" --name-only`,"Not a git repository or git is not installed.",t)}function rt(e,t){let n;try{n=D(`git log --since="${e} months ago" --numstat --format=`,{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:t})}catch{throw new Error("Not a git repository or git is not installed.")}let o=new Map;for(let i of n.toString().split(` | ||
| `)){if(!i)continue;let s=i.split(" ");if(s.length<3)continue;let r=s[0],l=s[1];if(r==="-"||l==="-")continue;let u=M(s[2].trim());if(!u)continue;let c=parseInt(r,10)+parseInt(l,10);Number.isFinite(c)&&o.set(u,(o.get(u)??0)+c)}return o}var X=3;function st(e,t){let n;try{n=D("git log --format=%ct --name-only --no-merges",{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:t})}catch{throw new Error("Not a git repository or git is not installed.")}let o=e*ce,i=Math.floor(Date.now()/1e3)-o*86400,s=o*X,r=new Map,l=new Map,u=null;for(let p of n.toString().split(` | ||
| `)){if(!p)continue;if(/^\d+$/.test(p)){u=parseInt(p,10);continue}if(u===null)continue;let a=M(p.trim());if(a)if(u>=i){let f=l.get(a);(f===void 0||u<f)&&l.set(a,u)}else{let f=r.get(a);(f===void 0||u>f)&&r.set(a,u)}}let c=new Map;for(let[p,a]of l){let f=r.get(p);if(f===void 0)continue;let g=Math.floor((a-f)/86400);g<s||c.set(p,{dormancyDays:g,dormancyMultiple:Math.round(g/o*10)/10,lastTouchedBeforeWindow:f,firstTouchedInWindow:a})}return c}function at(e,t){return be(`git log --since="${e} months ago" --grep="^fix" --format="" --name-only`,"Not a git repository or git is not installed.",t)}function ct(e){let t=new Set,n=[];for(let o of e.split(" ")){let i=o.trim();if(!i)continue;let s=i.match(/^(.+?)\s*<[^>]+>\s*$/),r=(s?s[1]:i).trim();!r||r.endsWith("[bot]")||t.has(r)||(t.add(r),n.push(r))}return n}function lt(e,t){let n;try{n=D(`git log --since="${e} months ago" --format="COMMIT_SEP%n%aN%x09%(trailers:key=Co-authored-by,valueonly,separator=%x09)" --name-only`,{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:t})}catch{throw new Error("Not a git repository or git is not installed.")}let o=new Map,i=n.toString().split(`COMMIT_SEP | ||
| import{execSync as Ke,spawnSync as Ve}from"child_process";import{existsSync as K,realpathSync as qe,writeFileSync as Pt}from"fs";import{relative as jt}from"path";import{Command as Bt,InvalidArgumentError as Ye}from"commander";import{execSync as D,spawnSync as ee}from"child_process";import{existsSync as et,mkdtempSync as tt,readFileSync as we,rmSync as ge}from"fs";import{tmpdir as nt}from"os";import{join as ne}from"path";var ot=[".obsignore",".obsceneignore"];function ye(){for(let e of ot)try{return we(e,"utf-8").split(` | ||
| `).map(n=>n.trim()).filter(n=>n!==""&&!n.startsWith("#"))}catch(t){if(t&&typeof t=="object"&&"code"in t&&t.code==="ENOENT")continue;throw t}return[]}var oe=[{title:"Test files and test infrastructure",patterns:[{pattern:"*.test.*",comment:"Unit test files"},{pattern:"*.spec.*",comment:"Spec test files"},{pattern:"*.integration.test.*",comment:"Integration tests"},{pattern:"test-setup.*",comment:"Test setup files"},{pattern:"test-utils.*",comment:"Test utility files"},{pattern:"test-helpers.*",comment:"Test helper files"},{pattern:"__tests__/**",comment:"Test directories"},{pattern:"__mocks__/**",comment:"Mock directories"},{pattern:"*.stories.*",comment:"Storybook stories"},{pattern:"*.d.ts",comment:"TypeScript declaration files"}]},{title:"Lock files and package manifests",patterns:[{pattern:"package.json",comment:"npm package manifest"},{pattern:"package-lock.json",comment:"npm lock file"},{pattern:"pnpm-lock.yaml",comment:"pnpm lock file"},{pattern:"yarn.lock",comment:"Yarn lock file"},{pattern:"bun.lock",comment:"Bun lock file"}]}],it=.5,rt=.8,Q=5,Z=3,H={complexity:{weak:3,plausible:10,acceptable:30},nesting:{weak:3,plausible:10,acceptable:30},defects:{weak:5,plausible:15,acceptable:50},authors:{weak:2,plausible:4,acceptable:8},coupling:{weak:5,plausible:30,acceptable:100}},T={complexity:"Engineering judgment: any rank ordering needs \u2265 3 items to be meaningful; higher tiers scale from there. No paper prescribes these exact cutoffs.",nesting:"Engineering judgment, informed by Campbell (SonarSource 2018) Cognitive Complexity which assigns a compounding penalty per nesting level. The 3/10/30 sample-count tiers are not from the paper.",defects:"code-maat's --min-revs default of 5 (Adam Tornhill); higher tiers are engineering judgment. Gall et al. (IWPSE 2003) and Hassan (ICSE 2009) study co-change and change-entropy but do not prescribe a specific commit-count floor.",authors:"Engineering judgment. Bird et al. (FSE 2011) Don't Touch My Code! shows minor contributors (< 5% of commits) correlate with elevated defects, motivating attention to contributor count \u2014 but the 2/4/8 tiers here are not from the paper.",coupling:"code-maat defaults (--min-revs 5, --max-changeset-size 30, Adam Tornhill). CodeScene's documented temporal-coupling default filters files with fewer than 10 commits. The 30/100 upper tiers are engineering judgment.",composite:"Reciprocal Rank Fusion (Cormack et al., SIGIR 2009) fuses multiple independent rankings; min-of-inputs is a strict monotone aggregator \u2014 when every input ranking is at confidence level L, the composite cannot exceed L."};function L(e,t,n,o,i){let s;return t<n.weak?s="inconclusive":t<n.plausible?s="weak":t<n.acceptable?s="plausible":s="acceptable",{level:s,reason:i(s),inputs:{metric:e,value:t,thresholds:n},source:o}}function be(e,t){return t.some(n=>n.test(e))}function Ce(e){let t=e.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,"\u27E8GLOBSTAR\u27E9").replace(/\*/g,"[^/]*").replace(/⟨GLOBSTAR⟩/g,".*").replace(/\?/g,".");return new RegExp(t)}function O(e){let t=e.replaceAll("\\","/");return t.startsWith("./")?t.slice(2):t}function N(e=[],t){let n=e.map(Ce),o;try{o=D("scc --by-file --format json --no-cocomo --no-gen",{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:t})}catch(r){throw r&&typeof r=="object"&&"code"in r&&r.code==="ENOENT"?new Error("scc not found. Install it: https://github.com/boyter/scc#install"):r}let i=JSON.parse(o.toString()),s=[];for(let r of i)for(let l of r.Files){let u=O(l.Location);be(u,n)||s.push({file:u,code:l.Code,lines:l.Lines,complexity:l.Complexity,comments:l.Comment,complexityDensity:l.Code>0?Math.round(l.Complexity/l.Code*100)/100:0})}return s.sort((r,l)=>l.complexity-r.complexity)}function xe(e,t,n){let o;try{o=D(e,{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:n})}catch{throw new Error(t)}let i=new Map;for(let s of o.toString().split(` | ||
| `)){let r=O(s.trim());r&&i.set(r,(i.get(r)??0)+1)}return i}function j(e,t){return xe(`git log --since="${e} months ago" --format="" --name-only`,"Not a git repository or git is not installed.",t)}function st(e,t){let n;try{n=D(`git log --since="${e} months ago" --numstat --format=`,{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:t})}catch{throw new Error("Not a git repository or git is not installed.")}let o=new Map;for(let i of n.toString().split(` | ||
| `)){if(!i)continue;let s=i.split(" ");if(s.length<3)continue;let r=s[0],l=s[1];if(r==="-"||l==="-")continue;let u=O(s[2].trim());if(!u)continue;let c=parseInt(r,10)+parseInt(l,10);Number.isFinite(c)&&o.set(u,(o.get(u)??0)+c)}return o}var te=3;function at(e,t){let n;try{n=D("git log --format=%ct --name-only --no-merges",{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:t})}catch{throw new Error("Not a git repository or git is not installed.")}let o=e*ue,i=Math.floor(Date.now()/1e3)-o*86400,s=o*te,r=new Map,l=new Map,u=null;for(let p of n.toString().split(` | ||
| `)){if(!p)continue;if(/^\d+$/.test(p)){u=parseInt(p,10);continue}if(u===null)continue;let a=O(p.trim());if(a)if(u>=i){let f=l.get(a);(f===void 0||u<f)&&l.set(a,u)}else{let f=r.get(a);(f===void 0||u>f)&&r.set(a,u)}}let c=new Map;for(let[p,a]of l){let f=r.get(p);if(f===void 0)continue;let g=Math.floor((a-f)/86400);g<s||c.set(p,{dormancyDays:g,dormancyMultiple:Math.round(g/o*10)/10,lastTouchedBeforeWindow:f,firstTouchedInWindow:a})}return c}function ct(e,t){return xe(`git log --since="${e} months ago" --grep="^fix" --format="" --name-only`,"Not a git repository or git is not installed.",t)}function lt(e){let t=new Set,n=[];for(let o of e.split(" ")){let i=o.trim();if(!i)continue;let s=i.match(/^(.+?)\s*<[^>]+>\s*$/),r=(s?s[1]:i).trim();!r||r.endsWith("[bot]")||t.has(r)||(t.add(r),n.push(r))}return n}function ut(e,t){let n;try{n=D(`git log --since="${e} months ago" --format="COMMIT_SEP%n%aN%x09%(trailers:key=Co-authored-by,valueonly,separator=%x09)" --name-only`,{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:t})}catch{throw new Error("Not a git repository or git is not installed.")}let o=new Map,i=n.toString().split(`COMMIT_SEP | ||
| `);for(let s of i){if(!s.trim())continue;let r=s.split(` | ||
| `),l=ct(r[0]);if(l.length!==0)for(let u=1;u<r.length;u++){let c=M(r[u].trim());if(!c)continue;let p=o.get(c);p||(p=new Map,o.set(c,p));for(let a of l)p.set(a,(p.get(a)??0)+1)}}return o}var ut=20;function ee(e,t=[],n){let o=t.map(Ce),i;try{i=D(`git log --since="${e} months ago" --format="COMMIT_SEP%n" --name-only`,{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:n})}catch{throw new Error("Not a git repository or git is not installed.")}let s=new Map,r=i.toString().split(`COMMIT_SEP | ||
| `),l=lt(r[0]);if(l.length!==0)for(let u=1;u<r.length;u++){let c=O(r[u].trim());if(!c)continue;let p=o.get(c);p||(p=new Map,o.set(c,p));for(let a of l)p.set(a,(p.get(a)??0)+1)}}return o}var pt=20;function ie(e,t=[],n){let o=t.map(Ce),i;try{i=D(`git log --since="${e} months ago" --format="COMMIT_SEP%n" --name-only`,{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:n})}catch{throw new Error("Not a git repository or git is not installed.")}let s=new Map,r=i.toString().split(`COMMIT_SEP | ||
| `);for(let l of r){if(!l.trim())continue;let u=new Set;for(let p of l.split(` | ||
| `)){let a=M(p.trim());a&&(we(a,o)||u.add(a))}let c=[...u];if(!(c.length<2||c.length>ut))for(let p=0;p<c.length;p++)for(let a=p+1;a<c.length;a++){let[f,g]=c[p]<c[a]?[c[p],c[a]]:[c[a],c[p]],w=f.includes("/")?f.slice(0,f.lastIndexOf("/")):"",k=g.includes("/")?g.slice(0,g.lastIndexOf("/")):"";if(w===k)continue;let d=`${f}\0${g}`;s.set(d,(s.get(d)??0)+1)}}return s}var te=5,ne=50,pt=5;function xe(e,t,n,o={}){let i=o.minCochanges??te,s=o.minDegree??ne,r=o.maxResults??pt,l=new Map;for(let[u,c]of e){if(c<i)continue;let[p,a]=u.split("\0"),f=n.has(p),g=n.has(a);if(f===g)continue;let w=t.get(p)??0,k=t.get(a)??0,d=Math.min(w,k);if(d===0)continue;let m=Math.max(w,k);if(c/m>=.9)continue;let x=Math.round(c/d*1e3)/10;if(x<s)continue;let h=f?p:a,b=f?a:p,S=l.get(h);(!S||x>S.degree)&&l.set(h,{file:h,partner:b,cochanges:c,degree:x})}return[...l.values()].sort((u,c)=>c.degree-u.degree||c.cochanges-u.cochanges||u.file.localeCompare(c.file)).slice(0,r)}function j(e,t){let n=0;for(let o of e){o.percentOfTotal=Math.round(o.score/t*1e3)/10,n+=o.score;let i=n/t;i<=ot?o.tier="hot":i<=it?o.tier="warm":o.tier="cool"}}var oe=[{key:"complexity",label:"Complexity \xD7 Churn",scoreFormula:"complexity \xD7 churn"},{key:"nesting",label:"Nesting \xD7 Churn",scoreFormula:"maxNesting \xD7 churn"},{key:"defects",label:"Fix Activity \xD7 Churn",scoreFormula:"fixes \xD7 churn"},{key:"authors",label:"Authors \xD7 Churn",scoreFormula:"authors \xD7 churn"}];function ft(e,t,n,o){let i=e.map(r=>{let l=t.get(r.file)??0,u=n(r);return{file:r.file,score:u*l,percentOfTotal:0,tier:"cool",churn:l,metricValue:u,metricDensity:o?o(r):void 0}}).filter(r=>r.score>0).sort((r,l)=>l.score-r.score),s=i.reduce((r,l)=>r+l.score,0);return s===0?[]:(j(i,s),i)}var mt=.05,dt=2;function gt(e){if(!e||e.size===0)return null;let t=0;for(let i of e.values())t+=i;if(t<dt)return null;let n=t*mt,o=0;for(let i of e.values())i<n&&o++;return o}function ht(e,t,n,o,i,s,r){let l={complexity:{extract:m=>m.complexity,density:m=>m.complexityDensity},nesting:{extract:m=>m.complexity===0?0:o.get(m.file)??0},defects:{extract:m=>n.get(m.file)??0,density:m=>{let x=n.get(m.file)??0;return m.code>0?Math.round(x/m.code*1e4)/1e4:0}},authors:{extract:m=>i.get(m.file)??0}},u={},c={},p=0;for(let m of e)m.complexity>0&&p++;c.complexity=A("filesWithComplexity",p,H.complexity,T.complexity,m=>m==="inconclusive"?`${p} files with measurable complexity \u2014 not enough to rank.`:`${p} files with measurable complexity (${m.toUpperCase()} sample size).`);let a=0;for(let m of e)m.complexity>0&&(o.get(m.file)??0)>=3&&a++;c.nesting=A("filesWithNesting>=3",a,H.nesting,T.nesting,m=>m==="inconclusive"?`${a} files with nesting depth \u2265 3 \u2014 not enough to rank.`:`${a} files with nesting depth \u2265 3 (${m.toUpperCase()} sample size).`);let f=[...n.values()].reduce((m,x)=>m+x,0),g=n.size,w=f<J||g<K;c.defects=A("fixCommits",f,H.defects,T.defects,m=>m==="inconclusive"||w?`${f} fix: commits across ${g} files \u2014 need \u2265 ${J} commits across \u2265 ${K} files (matches code-maat's --min-revs default).`:`${f} fix: commits across ${g} files (${m.toUpperCase()} sample size).`),w&&(c.defects={...c.defects,level:"inconclusive"},u.defects={reason:`insufficient data (${f} fix: commits across ${g} files, need ${J}+ commits across ${K}+ files)`,suggestion:"Adopt conventional commits with fix: prefix. See conventionalcommits.org",confidence:c.defects});let k=0;for(let m of i.values())m>k&&(k=m);c.authors=A("maxAuthors",k,H.authors,T.authors,m=>m==="inconclusive"?`${k} distinct authors on the most-touched file \u2014 not enough to rank ownership.`:`${k} distinct authors on the most-touched file (${m.toUpperCase()} sample size).`),k<=1&&(c.authors={...c.authors,level:"inconclusive"},u.authors={reason:"all files have the same author count \u2014 no variance to rank",confidence:c.authors});let d={};for(let m of oe){if(u[m.key])continue;if(c[m.key].level==="inconclusive"){u[m.key]={reason:c[m.key].reason,confidence:c[m.key]};continue}let x=l[m.key],h=ft(e,t,x.extract,x.density);if(h.length===0)continue;if(m.key==="authors"&&r)for(let v of h)v.minorAuthors=gt(r.get(v.file));let b=s>0?h.slice(0,s):h,S={hot:0,warm:0,cool:0};for(let v of h)S[v.tier]++;d[m.key]={label:m.label,scoreFormula:m.scoreFormula,totalScore:h.reduce((v,F)=>v+F.score,0),tierCounts:S,tiers:S,totalEntries:h.length,showing:b.length,entries:b,confidence:c[m.key]}}return{rankings:d,skipped:u}}function ie(e){let t;try{t=D(`git diff --name-only ${e}...HEAD`,{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"]})}catch{throw new Error(`Failed to compute diff against base ref '${e}'. Verify the ref exists (e.g. 'git rev-parse --verify <ref>').`)}let n=new Set;for(let o of t.toString().split(` | ||
| `)){let i=M(o.trim());i&&n.add(i)}return n}var yt=["main","master"];function ke(){for(let e of yt)try{return D(`git rev-parse --verify ${e}`,{stdio:["pipe","pipe","pipe"]}),e}catch{}}function Se(){try{return D("git rev-parse --show-toplevel",{stdio:["pipe","pipe","pipe"]}).toString().trim()||void 0}catch{return}}function wt(e,t){let n=new Map;if(t.length===0)return n;let o=t.filter(r=>Ze(Q(e,r)));if(o.length===0)return n;let i=Y("scc",["--by-file","--format","json","--no-cocomo","--no-gen",...o],{cwd:e,maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"]});if(i.error&&"code"in i.error&&i.error.code==="ENOENT")throw new Error("scc not found. Install it: https://github.com/boyter/scc#install");if(i.error)throw new Error(`scc spawn failed: ${i.error.message}`);if(i.status!==0)throw new Error(`scc failed on base worktree: ${i.stderr?.toString().trim()||"unknown error"}`);let s=JSON.parse(i.stdout.toString());for(let r of s)for(let l of r.Files){let u=M(l.Location);n.set(u,{file:u,code:l.Code,lines:l.Lines,complexity:l.Complexity,comments:l.Comment,complexityDensity:l.Code>0?Math.round(l.Complexity/l.Code*100)/100:0})}return n}function B(e,t){let n=et(Q(tt(),"obscene-base-")),o={...process.env};for(let s of Object.keys(o))s.startsWith("GIT_")&&delete o[s];let i=Y("git",["worktree","add","--detach",n,e],{stdio:["pipe","pipe","pipe"],env:o});if(i.status!==0){de(n,{recursive:!0,force:!0});let s=i.stderr?.toString().trim();throw new Error(`Could not create worktree at '${e}'${s?`: ${s}`:""}. Verify the ref exists (e.g. 'git rev-parse --verify <ref>').`)}try{return t(n)}finally{Y("git",["worktree","remove","--force",n],{stdio:["pipe","pipe","pipe"],env:o}).status!==0&&de(n,{recursive:!0,force:!0})}}function $e(e,t,n){let o=new Map;if(t.length===0)return o;let i=B(e,s=>wt(s,t));for(let s of t){let r=n.get(s);if(r===void 0)continue;let l=i.get(s);l===void 0?o.set(s,{oldComplexity:null,newComplexity:r,change:null}):o.set(s,{oldComplexity:l.complexity,newComplexity:r,change:r-l.complexity})}return o}function re(e){let t;try{t=D("git ls-files",{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:e})}catch{throw new Error("Not a git repository or git is not installed.")}let n=new Set;for(let o of t.toString().split(` | ||
| `)){let i=M(o.trim());i&&n.add(i)}return n}function ve(e,t,n,o,i){let s=[];for(let[u,c]of e){if(c<o)continue;let[p,a]=u.split("\0"),f=t.get(p)??0,g=t.get(a)??0,w=Math.min(f,g),k=w>0?Math.round(c/w*1e3)/10:0,d=(n.get(p)??0)+(n.get(a)??0),m={file1:p,file2:a,cochanges:c,degree:k,totalComplexity:d,couplingScore:c,percentOfTotal:0,tier:"cool"},x=Math.max(f,g);c>0&&x>0&&c/x>=.9&&(m.lockstep=!0),i&&(i.has(p)||(m.file1Deleted=!0),i.has(a)||(m.file2Deleted=!0)),s.push(m)}s.sort((u,c)=>c.couplingScore-u.couplingScore);let r=s.reduce((u,c)=>u+c.couplingScore,0);if(r===0)return[];let l=s.map(u=>({...u,score:u.couplingScore}));j(l,r);for(let u=0;u<s.length;u++)s[u].percentOfTotal=l[u].percentOfTotal,s[u].tier=l[u].tier;return s}function Re(e,t,n,o){let i=new Map,s=new Map;for(let[c,p]of e){if(p<t)continue;let[a,f]=c.split("\0");if(n){let g=Math.max(n.get(a)??0,n.get(f)??0);if(g>0&&p/g>=.9)continue}i.has(a)||i.set(a,new Set),i.has(f)||i.set(f,new Set),i.get(a)?.add(f),i.get(f)?.add(a),s.set(a,(s.get(a)??0)+p),s.set(f,(s.get(f)??0)+p)}let r=[];for(let[c,p]of i){let a={file:c,partners:p.size,strength:s.get(c),percentOfTotal:0,tier:"cool"};o&&!o.has(c)&&(a.fileDeleted=!0),r.push(a)}r.sort((c,p)=>p.strength!==c.strength?p.strength-c.strength:p.partners!==c.partners?p.partners-c.partners:c.file.localeCompare(p.file));let l=r.reduce((c,p)=>c+p.strength,0),u=r.map(c=>({...c,score:c.strength}));j(u,l);for(let c=0;c<r.length;c++)r[c].percentOfTotal=u[c].percentOfTotal,r[c].tier=u[c].tier;return r}function Ct(e,t){let n=new Map;for(let o of e){let i;try{i=he(t?Q(t,o):o,"utf-8")}catch{n.set(o,0);continue}let s=[],r=new Map,l=0;for(let a of i.split(` | ||
| `)){if(!a.trim())continue;let f=a.match(/^(\s+)/);if(!f){l=0;continue}let g=f[1];if(s.push(g),g.includes(" "))continue;let w=g.length,k=w-l;k>0&&r.set(k,(r.get(k)??0)+1),l=w}let u=4,c=0;for(let[a,f]of r)(f>c||f===c&&a<u)&&(c=f,u=a);let p=0;for(let a of s){let f=0;for(let g of a)g===" "?f+=1:g===" "&&(f+=1/u);f=Math.floor(f),f>p&&(p=f)}n.set(o,p)}return n}var bt=[{dir:".github",pattern:".github/**",comment:"GitHub Actions and workflows"},{dir:".circleci",pattern:".circleci/**",comment:"CircleCI configuration"},{dir:".husky",pattern:".husky/**",comment:"Git hooks"},{dir:".vscode",pattern:".vscode/**",comment:"VS Code settings"},{dir:".idea",pattern:".idea/**",comment:"JetBrains settings"},{dir:"scripts",pattern:"scripts/**",comment:"Build and utility scripts"},{dir:"docs",pattern:"docs/**",comment:"Documentation"},{dir:"docker",pattern:"docker/**",comment:"Docker configuration"},{dir:"fixtures",pattern:"fixtures/**",comment:"Test fixtures"},{dir:"vendor",pattern:"vendor/**",comment:"Vendored dependencies"}],xt=[{test:/\.generated\./,pattern:"*.generated.*",comment:"Generated code"},{test:/\.gen\.[^.]+$/,pattern:"*.gen.*",comment:"Generated code"},{test:/\.config\.\w/,pattern:"*.config.*",comment:"Configuration files"},{test:/(?:^|\/)\.gitlab-ci/,pattern:".gitlab-ci*",comment:"GitLab CI configuration"},{test:/^\.claude\/commands\//,pattern:".claude/commands/**",comment:"Claude Code slash commands (often generated from sources)"},{test:/^\.opencode\/commands\//,pattern:".opencode/commands/**",comment:"OpenCode slash commands (often generated from sources)"},{test:/^\.cursor\/rules\//,pattern:".cursor/rules/**",comment:"Cursor rules (often generated from sources)"}];function De(){let e=re(),t=[],n=new Set;for(let o of e){let i=o.indexOf("/");i>0&&n.add(o.slice(0,i))}for(let o of bt)n.has(o.dir)&&t.push({pattern:o.pattern,comment:o.comment});for(let o of xt)for(let i of e)if(o.test.test(i)){t.push({pattern:o.pattern,comment:o.comment});break}return t}function Ee(e,t=Z){let n=["# Generated by obscene init","# Edit this file to customize which files are excluded from analysis.","# Patterns use glob syntax (same as .gitignore).","# See: https://github.com/wbern/obscene#ignore-files",""];for(let o of t){n.push(`# ${o.title}`);for(let i of o.patterns)n.push(i.pattern);n.push("")}if(e.length>0){n.push("# Project-specific patterns");for(let o of e)n.push(`# ${o.comment}`),n.push(o.pattern);n.push("")}return n.join(` | ||
| `)}var kt=10,ge={inconclusive:0,weak:1,plausible:2,acceptable:3};function St(e){let t=Object.values(e).map(i=>i.confidence),n=t.length;if(n<2)return{level:"inconclusive",reason:`${n} input ranking \u2014 RRF requires \u2265 2 independent rankings.`,inputs:{metric:"inputRankings",value:n,thresholds:{weak:2,plausible:3,acceptable:4}},source:T.composite};let o="acceptable";for(let i of t)ge[i.level]<ge[o]&&(o=i.level);return{level:o,reason:`Composite inherits min-of-inputs across ${n} rankings (weakest: ${o.toUpperCase()}).`,inputs:{metric:"inputRankings",value:n,thresholds:{weak:2,plausible:3,acceptable:4}},source:T.composite}}function $t(e,t,n){let o=Object.keys(e).length,i=St(e),s=new Map;for(let p of Object.values(e))for(let a=0;a<p.entries.length;a++){let f=p.entries[a].file,g=1/(kt+a+1),w=s.get(f);w?(w.score+=g,w.dims+=1):s.set(f,{score:g,dims:1})}let r=[];for(let[p,a]of s)r.push({file:p,score:Math.round(a.score*1e4)/1e4,percentOfTotal:0,tier:"cool",churn:t.get(p)??0,dimensionCount:a.dims});r.sort((p,a)=>a.score-p.score);let l=r.reduce((p,a)=>p+a.score,0);if(l===0)return{label:"Combined",scoreFormula:"reciprocal rank fusion across all dimensions",totalScore:0,tierCounts:{hot:0,warm:0,cool:0},tiers:{hot:0,warm:0,cool:0},totalDimensions:o,totalEntries:0,showing:0,entries:[],confidence:i};j(r,l);let u=n>0?r.slice(0,n):r,c={hot:0,warm:0,cool:0};for(let p of r)c[p.tier]++;return{label:"Combined",scoreFormula:"reciprocal rank fusion across all dimensions",totalScore:Math.round(l*1e4)/1e4,tierCounts:c,tiers:c,totalDimensions:o,totalEntries:r.length,showing:u.length,entries:u,confidence:i}}function L(e,t,n,o,i="commits"){let s=i==="lines"?rt(t,o):W(t,o),r=at(t,o),l=lt(t,o),u=st(t,o),c=new Map;for(let[h,b]of l)c.set(h,b.size);let p=Ct(e.map(h=>h.file),o),{rankings:a,skipped:f}=ht(e,s,r,p,c,n,l),g=$t(a,s,n),w=0;for(let h of e)w+=h.complexity;let k=t*ce,d=new Map(e.map(h=>[h.file,h])),m=[];for(let[h,b]of u){let S=d.get(h);S&&m.push({file:h,dormancyDays:b.dormancyDays,dormancyMultiple:b.dormancyMultiple,lastTouchedBeforeWindow:b.lastTouchedBeforeWindow,firstTouchedInWindow:b.firstTouchedInWindow,complexity:S.complexity,churn:s.get(h)??0})}m.sort((h,b)=>b.dormancyMultiple-h.dormancyMultiple||h.file.localeCompare(b.file));let x={windowDays:k,minDormancyMultiple:X,minDormancyDays:k*X,entries:m};return{rankings:a,skipped:f,composite:g,corpus:{fileCount:e.length,totalComplexity:w},churn:s,reawakened:x}}function Me(e,t){if(t<=0)return e;let n={};for(let[i,s]of Object.entries(e.rankings)){let r=s.entries.slice(0,t);n[i]={...s,entries:r,showing:r.length}}let o=e.composite.entries.slice(0,t);return{rankings:n,skipped:e.skipped,composite:{...e.composite,entries:o,showing:o.length},corpus:e.corpus,churn:e.churn,reawakened:e.reawakened}}function se(e){let t=N(e.excludes,e.cwd),n=L(t,e.months,0,e.cwd,e.churnMode);return{files:t,rankings:n.rankings,skipped:n.skipped,composite:n.composite,reawakened:n.reawakened,corpus:n.corpus}}function ae(e,t,n,o){let i=new Map;for(let d of n.composite.entries)i.set(d.file,{score:d.score,tier:d.tier});let s=new Map;for(let d of o.composite.entries)s.set(d.file,{score:d.score,tier:d.tier});let r=new Set(n.files.map(d=>d.file)),l=new Set(o.files.map(d=>d.file)),u=[],c=[];for(let d of l)r.has(d)||u.push(d);for(let d of r)l.has(d)||c.push(d);u.sort(),c.sort();let p=new Set([...i.keys(),...s.keys()]),a=[],f=[],g=[],w=[],k=[];for(let d of p){let m=i.get(d),x=s.get(d),h=m?.score??null,b=x?.score??null,S=m?.tier??null,v=x?.tier??null,F=h!==null&&b!==null?b-h:null,Qe=F!==null&&h!==null&&h!==0?Math.round(F/h*1e3)/10:null,R;S===null?R="new":v===null?R="deleted":S!=="hot"&&v==="hot"?R="entered-hot":S==="cool"&&v==="warm"?R="entered-warm":S==="hot"&&v!=="hot"?R="exited-hot":S==="warm"&&v==="cool"?R="exited-warm":R="stable",R==="entered-hot"?f.push(d):R==="entered-warm"?g.push(d):R==="exited-hot"?w.push(d):R==="exited-warm"&&k.push(d),a.push({file:d,oldScore:h,newScore:b,change:F!==null?Math.round(F*1e4)/1e4:null,percentChange:Qe,oldTier:S,newTier:v,transition:R})}return a.sort((d,m)=>{let x=Math.abs(d.change??0),h=Math.abs(m.change??0);return x!==h?h-x:d.file.localeCompare(m.file)}),f.sort(),g.sort(),w.sort(),k.sort(),{base:e,head:t,newFiles:u,deletedFiles:c,tierTransitions:{enteredHot:f,enteredWarm:g,exitedHot:w,exitedWarm:k},scoreChanges:a,perDimensionDeltas:{complexity:{oldTotal:n.corpus.totalComplexity,newTotal:o.corpus.totalComplexity,change:o.corpus.totalComplexity-n.corpus.totalComplexity},fileCount:{oldTotal:n.corpus.fileCount,newTotal:o.corpus.fileCount,change:o.corpus.fileCount-n.corpus.fileCount}}}}function Oe(e){return A("commitsInWindow",e,H.coupling,T.coupling,t=>t==="inconclusive"?`${e} commits in window \u2014 need \u2265 ${H.coupling.weak} (matches code-maat's --min-revs default).`:`${e} commits in window (${t.toUpperCase()} sample size).`)}function Te(e,t){try{let n=D(`git rev-list --count --since="${e} months ago" HEAD`,{stdio:["pipe","pipe","pipe"],cwd:t});return parseInt(n.toString().trim(),10)||0}catch{throw new Error("Not a git repository or git is not installed.")}}var ce=30;function Ie(e,t){let n=e*ce,o;try{let l=D("git log --format=%ct --reverse HEAD",{maxBuffer:52428800,stdio:["pipe","pipe","pipe"],cwd:t}).toString().split(` | ||
| `,1)[0].trim();if(o=parseInt(l,10),!Number.isFinite(o)||o<=0)return{windowDays:n,spanDays:0,underCovered:!0}}catch{throw new Error("Not a git repository or git is not installed.")}let i=Math.floor(Date.now()/1e3),s=Math.max(0,Math.floor((i-o)/86400));return{windowDays:n,spanDays:s,underCovered:s<n}}import C from"picocolors";import O from"picocolors";var vt=/\x1b\[[0-9;]*m/g;function Rt(e){return e>=11904&&e<=12543||e>=12800&&e<=13311||e>=13312&&e<=40959||e>=44032&&e<=55215||e>=63744&&e<=64255||e>=65281&&e<=65376||e>=65504&&e<=65510||e>=9728&&e<=9983||e>=127744&&e<=129791||e>=131072&&e<=195103}function Fe(e){let t=e.replace(vt,""),n=0;for(let o of t){let i=o.codePointAt(0);i===65038||i===65039||(n+=Rt(i)?2:1)}return n}function $(e,t){let n=Fe(e);return n>=t?e:e+" ".repeat(t-n)}function y(e,t){let n=Fe(e);return n>=t?e:" ".repeat(t-n)+e}function E(e,t){if(t<=0)return"";if(e.length<=t)return e;if(t===1)return"\u2026";let n=t-1,o=Math.ceil(n*.6),i=n-o;return`${e.slice(0,i)}\u2026${e.slice(e.length-o)}`}function _(e){return e==="hot"?O.red("\u{1F525} HOT "):e==="warm"?O.yellow("\u2600\uFE0F WARM"):O.blue("\u{1F9CA} COOL")}function P(e,t){return e==="hot"?O.red(t):e==="warm"?O.yellow(t):O.blue(t)}function U(e,t,n){let o=[];return o.push(`Tiers: ${O.red(`${e.hot} HOT`)}, ${O.yellow(`${e.warm} WARM`)}, ${O.blue(`${e.cool} COOL`)}`),o.push(`Showing: ${t} of ${n}`),o}var Dt={inconclusive:C.gray,weak:C.yellow,plausible:C.cyan,acceptable:C.green};function G(e){let t=Dt[e.level];return[t(`Confidence: ${e.level.toUpperCase()} \u2014 ${e.reason}`)]}var Et=Object.fromEntries(oe.map(e=>[e.key,e.label]));function He(e){let t=[],{summary:n,files:o}=e;t.push(`Complexity Report \u2014 ${n.fileCount} files, ${n.totalComplexity} total complexity`),t.push(`Showing: ${n.showing} | Avg complexity/file: ${n.avgComplexityPerFile}`),t.push(""),t.push($("File",60)+y("Code",8)+y("Complexity",12)+y("Density",9)+y("Comments",10)),t.push("\u2500".repeat(99));for(let i of o)t.push($(E(i.file,58),60)+y(String(i.code),8)+y(String(i.complexity),12)+y(i.complexityDensity.toFixed(2),9)+y(String(i.comments),10));return t.push(""),t.push(C.dim("Complexity=cyclomatic branch/loop count | Density=complexity/code | Comments=comment lines")),t.push(C.dim("High complexity is expected for parsers, state machines, and business logic. Compare density across files, not raw values.")),t.push(C.dim("Docs: https://github.com/wbern/obscene#metrics")),t.join(` | ||
| `)}function Ne(e){return e?e.oldComplexity===null||e.change===null?"new":e.change===0?"0":e.change>0?`+${e.change}`:String(e.change):"\xB7"}function Mt(e,t){let n=[{header:"File",width:50,align:"left",value:r=>E(r.file,48)},{header:"Score",width:8,align:"right",value:r=>r.score.toLocaleString()},{header:"%",width:7,align:"right",value:r=>r.percentOfTotal.toFixed(1)},{header:"Churn",width:7,align:"right",value:r=>String(r.churn)}],o={complexity:[{header:"Cmplx",width:7,align:"right",value:r=>String(r.metricValue)},{header:"Dens",width:7,align:"right",value:r=>(r.metricDensity??0).toFixed(2)}],nesting:[{header:"Nest",width:6,align:"right",value:r=>String(r.metricValue)}],defects:[{header:"Fixes",width:6,align:"right",value:r=>String(r.metricValue)},{header:"FxDns",width:7,align:"right",value:r=>(r.metricDensity??0).toFixed(4)}],authors:[{header:"Auth",width:6,align:"right",value:r=>String(r.metricValue)},{header:"MinAuth",width:9,align:"right",value:r=>r.minorAuthors===null||r.minorAuthors===void 0?"\u2014":String(r.minorAuthors)}]},i={header:"Tier",width:12,align:"right",value:r=>_(r.tier)},s={header:"\u0394",width:7,align:"right",value:r=>Ne(r.complexityDelta)};return[...n,...o[e]??[],...t?[s]:[],i]}var Ot={complexity:"\u{1F9EC}",nesting:"\u{1F4CF}",defects:"\u{1F527}",authors:"\u{1F465}"};function Tt(e,t,n,o){let i=[],s=Mt(e,o),r=Ot[e],l=r?`${r} `:"",u=t.label.toUpperCase().replace("CHURN","\u{1F504} CHURN");if(i.push(`${l}${u} \u2014 Total score: ${t.totalScore.toLocaleString()}`),i.push(...G(t.confidence)),n)for(let a of n.split(` | ||
| `))i.push(C.dim(a));i.push(...U(t.tierCounts,t.showing,t.totalEntries)),i.push("");let c=s.map(a=>a.align==="left"?$(a.header,a.width):y(a.header,a.width)).join("");i.push(c);let p=s.reduce((a,f)=>a+f.width,0);i.push("\u2500".repeat(p));for(let a of t.entries){let g=s.map(w=>{let k=w.value(a);return w.align==="left"?$(k,w.width):y(k,w.width)}).join("");i.push(P(a.tier,g))}return i}function It(e){let t=[];t.push(""),t.push(C.cyan(`Full Delta \u2014 ${e.base} \u2192 ${e.head}`)),t.push(C.dim("Tier transitions are relative percentile bands \u2014 a file can shift tiers because its absolute score moved OR because the rest of the corpus moved around it. scoreChanges carries the absolute delta."));let{enteredHot:n,enteredWarm:o,exitedHot:i,exitedWarm:s}=e.tierTransitions;if(n.length===0&&o.length===0&&i.length===0&&s.length===0&&e.newFiles.length===0&&e.deletedFiles.length===0)t.push(C.dim("No tier transitions, no new/deleted files."));else{let p=(a,f)=>{if(f.length!==0){t.push(a);for(let g of f.slice(0,10))t.push(` ${E(g,80)}`);f.length>10&&t.push(C.dim(` \u2026 and ${f.length-10} more`))}};p(C.red(` \u2191 entered HOT (${n.length}):`),n),p(C.yellow(` \u2191 entered WARM (${o.length}):`),o),p(C.green(` \u2193 cooled out of HOT (${i.length}):`),i),p(C.green(` \u2193 cooled out of WARM (${s.length}):`),s),p(C.cyan(` + new files (${e.newFiles.length}):`),e.newFiles),p(C.cyan(` \u2212 deleted files (${e.deletedFiles.length}):`),e.deletedFiles)}let r=e.perDimensionDeltas.complexity,l=e.perDimensionDeltas.fileCount,u=r.change>0?"+":"",c=l.change>0?"+":"";return t.push(""),t.push(C.dim(`Corpus: complexity ${r.oldTotal} \u2192 ${r.newTotal} (${u}${r.change}) \xB7 files ${l.oldTotal} \u2192 ${l.newTotal} (${c}${l.change})`)),t}function Ft(e){let t=[],{windowDays:n,minDormancyMultiple:o,minDormancyDays:i,entries:s}=e;t.push(C.magenta(`Reawakened \u2014 dormant \u2265 ${o}\xD7 window (${i}d) then touched again`)),t.push(C.dim(` Window: ${n}d \xB7 Rule: gap between last pre-window commit and first in-window commit \u2265 ${i}d`)),t.push("");let r=` ${$("File",40)} ${y("Dormancy",12)} ${y("\xD7Window",10)} ${y("Cx",6)} ${y("Churn",6)}`;t.push(C.dim(r));for(let l of s)t.push(` ${$(E(l.file,40),40)} ${y(`${l.dormancyDays}d`,12)} ${y(`${l.dormancyMultiple}\xD7`,10)} ${y(String(l.complexity),6)} ${y(String(l.churn),6)}`);return t}function le(e){let t=[],{churnWindow:n,churnMode:o,rankings:i,corpus:s,delta:r,fullDelta:l}=e;if(r&&(t.push(C.cyan(`Delta \u2014 ${r.changedFiles.length} file${r.changedFiles.length===1?"":"s"} changed since ${r.base}`)),r.fallback&&t.push(C.yellow(` \u26A0 full-delta unavailable \u2014 showing filtered view. Reason: ${r.fallback.reason}`)),r.changedFiles.length===0))return t.push(""),t.push(C.dim("No changes \u2014 nothing to rank.")),t.join(` | ||
| `);l&&t.push(...It(l));let u=o==="lines"?" (line-based)":"";t.push(`Hotspots \u2014 ${n} churn window${u}`),s&&s.fileCount>0&&s.totalComplexity===0&&(t.push(""),t.push(C.yellow("Note: no measurable code complexity detected across this corpus (cyclomatic = 0).")),t.push(C.yellow("Rankings reflect size and churn only \u2014 HOT/WARM/COOL are relative groupings, not risk labels."))),t.push("");let c=r!==void 0&&Object.values(i).some(f=>f.entries.some(g=>g.complexityDelta!==void 0)),p=Object.keys(i);for(let f=0;f<p.length;f++){let g=p[f];t.push(...Tt(g,i[g],e.guide[g],c)),f<p.length-1&&(t.push(""),t.push("\xB7 \xB7 \xB7"),t.push(""))}if(e.skipped)for(let[f,g]of Object.entries(e.skipped)){t.push("");let w=Et[f]??`${f.charAt(0).toUpperCase()+f.slice(1)} \xD7 Churn`;t.push(`${w} \u2014 skipped (${g.reason})`),g.suggestion&&t.push(` ${g.suggestion}`)}e.reawakened&&(t.push(""),t.push("\xB7 \xB7 \xB7"),t.push(""),t.push(...Ft(e.reawakened))),t.push(""),t.push(C.dim("Score=metric\xD7churn | Tiers are relative to THIS codebase, not absolute quality grades."));let a=s!==void 0&&s.fileCount>0&&s.totalComplexity===0;return t.push(C.dim(a?"High scores flag files that change often and are sizable \u2014 neither is bad in itself.":"High scores flag review candidates, not bad code \u2014 stable complex files (parsers, engines) score high naturally.")),t.push(C.dim("Docs: https://github.com/wbern/obscene#metrics")),s?.filtered===!1&&(t.push(""),t.push(C.yellow("\u26A0 Corpus unfiltered \u2014 no .obsignore found. Lockfiles, generated code, and vendored dependencies may dominate these rankings.")),t.push(C.dim(" Run `obscene init` to generate a starter .obsignore."))),t.join(` | ||
| `)}function Ae(e){let t=[],{tierCounts:n,totalScore:o,churnWindow:i,couplings:s}=e;t.push(`Coupling \u2014 ${i} churn window | Min shared: ${e.minCochanges} | Total score: ${o.toLocaleString()}`),t.push(...G(e.confidence)),t.push(...U(n,e.showing,e.totalCouplings)),t.push($("File 1",35)+$("File 2",35)+y("Shared",7)+y("Degree",8)+y("Cmplx",7)+y("Tier",12)),t.push("\u2500".repeat(104));let r=!1,l=!1;for(let u of s){(u.file1Deleted||u.file2Deleted)&&(r=!0),u.lockstep&&(l=!0);let c=u.file1Deleted?`\u2020 ${E(u.file1,31)}`:E(u.file1,33),p=u.file2Deleted?`\u2020 ${E(u.file2,31)}`:E(u.file2,33),a=u.lockstep?`${u.degree.toFixed(1)}\u21C4`:`${u.degree.toFixed(1)}%`,f=$(c,35)+$(p,35)+y(String(u.cochanges),7)+y(a,8)+y(String(u.totalComplexity),7)+y(_(u.tier),12);t.push(P(u.tier,f))}return t.push(""),t.push(C.dim("Shared=co-changed commits | Degree=shared/min(churn)\xD7100 | Cmplx=sum of both files")),r&&t.push(C.dim("\u2020 = file no longer present at HEAD (deleted or renamed)")),l&&t.push(C.dim("\u21C4 = lockstep pair (both files only ever changed together \u2014 signal is real but uninformative)")),t.push(C.dim("Tiers are relative to THIS codebase, not absolute quality grades. High coupling may be intentional and fine.")),t.push(C.dim("Same-directory pairs excluded. Commits touching >20 files skipped. Only cross-directory dependencies shown.")),t.push(C.dim("Docs: https://github.com/wbern/obscene#metrics")),e.sumOfCoupling&&e.sumOfCoupling.length>0&&(t.push(""),t.push(...Ht(e.sumOfCoupling,e.confidence))),t.join(` | ||
| `)}function Ht(e,t){let n=[];n.push("\u2500".repeat(68)),n.push(`${C.bold("Sum of Coupling")} ${C.dim("(experimental \u2014 not independently validated)")} \u2014 files whose couplings concentrate the most cross-dir change traffic`),n.push(...G(t)),n.push(""),n.push($("File",40)+y("Partners",10)+y("Strength",10)+y("Tier",8)),n.push("\u2500".repeat(68));let o=!1;for(let i of e){let s=i.fileDeleted?`\u2020 ${i.file}`:i.file;i.fileDeleted&&(o=!0);let r=$(E(s,38),40)+y(String(i.partners),10)+y(String(i.strength),10)+y(_(i.tier),8);n.push(P(i.tier,r))}return n.push(""),o&&n.push(C.dim("\u2020 = file no longer present at HEAD (deleted or renamed away); coupling signal is historical.")),n.push(C.dim("Partners=distinct cross-dir co-change partners | Strength=\u03A3 pair cochange counts (= code-maat's SoC analysis, filtered to cross-dir pairs and \u226420-file commits).")),n.push(C.dim(`Navigation aid: high strength means "worth a look at this file's couplings", not "this file is defect-prone".`)),n.push(C.dim("EXPERIMENTAL: NOT independently validated against defect data; may change, be reframed, or be removed.")),n}function Le(e){return e==="hot"?"HOT ":e==="warm"?"WARM":"COOL"}function _e(e){let t=e.historyCoverage;return t?.underCovered?` [history covers ~${t.spanDays}d, window ${t.windowDays}d]`:""}function Nt(e){return`${e} commit${e===1?"":"s"}`}function ue(e){let t=[],n=e.composite,o=e.corpus,i=o?`${o.fileCount} files, ${o.totalComplexity} total complexity`:"",s=`Hotspot landscape (composite RRF, ${e.churnWindow} window`+(i?`, ${i}`:"")+_e(e)+"):";if(t.push(s),!n||n.entries.length===0)return t.push("(no composite ranking \u2014 insufficient signal)"),t.join(` | ||
| `);t.push(`Confidence: ${n.confidence.level.toUpperCase()} \u2014 ${n.confidence.reason}`);for(let r of n.entries)t.push(`${Le(r.tier)} ${$(r.file,50)} ${y(`${r.percentOfTotal.toFixed(1)}%`,6)} ${y(Nt(r.churn),12)} ${r.dimensionCount}/${n.totalDimensions} dims`);return t.push(""),t.push("For volume-weighted churn: --churn-mode lines. For co-change pairs: obscene coupling."),t.join(` | ||
| `)}function Pe(e){let t=[],{summary:n,files:o}=e;t.push(`Complexity report \u2014 ${n.fileCount} files, ${n.totalComplexity} total complexity, ${n.avgComplexityPerFile} avg/file (showing ${n.showing}):`);for(let i of o)t.push(`${$(i.file,50)} complexity=${y(String(i.complexity),5)} density=${i.complexityDensity.toFixed(2)} code=${i.code}`);return t.join(` | ||
| `)}function We(e){let t=[],n=`Coupling \u2014 ${e.churnWindow} window, min shared: ${e.minCochanges}`+_e(e)+":";if(t.push(n),e.couplings.length===0)return t.push("(no pairs above thresholds)"),t.join(` | ||
| `);t.push(`Confidence: ${e.confidence.level.toUpperCase()} \u2014 ${e.confidence.reason}`);for(let o of e.couplings){let i=`${o.file1} \u2194 ${o.file2}`,s=o.lockstep?"\u21C4":"%";t.push(`${Le(o.tier)} ${$(i,70)} ${y(`${o.cochanges} shared`,10)} ${y(`${o.degree.toFixed(1)}${s}`,8)}`)}return t.join(` | ||
| `)}function je(e){let t=[],n=e.entries.some(s=>s.complexityDelta!==void 0),o=n?91:84;t.push("\u2550".repeat(o)),t.push(`\u2605 ${e.label.toUpperCase()} \u2014 Total score: ${e.totalScore.toLocaleString()}`),t.push(...G(e.confidence)),t.push(...U(e.tierCounts,e.showing,e.totalEntries)),t.push("");let i=$("File",50)+y("Score",9)+y("Churn",7)+y("Dims",6);n&&(i+=y("\u0394",7)),i+=y("Tier",12),t.push(i),t.push("\u2500".repeat(o));for(let s of e.entries){let r=$(E(s.file,48),50)+y(s.score.toFixed(4),9)+y(String(s.churn),7)+y(`${s.dimensionCount}/${e.totalDimensions}`,6);n&&(r+=y(Ne(s.complexityDelta),7)),r+=y(_(s.tier),12),t.push(P(s.tier,r))}return t.join(` | ||
| `)}var At=25,Lt=new Set(["SessionStart","Setup","SubagentStart","UserPromptSubmit","UserPromptExpansion","PreToolUse","PostToolUse","PostToolUseFailure","PostToolBatch"]);function Be(e){return`${e>0?"+":""}${e.toFixed(0)}%`}function Ue(e,t={}){let n=t.significantPercentChange??At,o=new Set([...e.tierTransitions.enteredHot,...e.tierTransitions.enteredWarm,...e.tierTransitions.exitedHot,...e.tierTransitions.exitedWarm]),i=new Map(e.scoreChanges.map(d=>[d.file,d])),s=[];for(let d of[...o].sort()){let m=i.get(d);if(!m)continue;let x=m.oldTier??"\u2014",h=m.newTier??"\u2014",b=m.percentChange!==null?Be(m.percentChange):"\u2014";s.push(`- ${d}: ${x} \u2192 ${h} (score ${b})`)}let r=e.scoreChanges.filter(d=>!o.has(d.file)).filter(d=>d.percentChange!==null&&Math.abs(d.percentChange)>=n).sort((d,m)=>Math.abs(m.percentChange)-Math.abs(d.percentChange));for(let d of r){let m=d.newTier??d.oldTier??"\u2014";s.push(`- ${d.file}: score ${Be(d.percentChange)} (stayed ${m})`)}let l=_t(t.reminders,t.reminderMinDegree);if(s.length===0&&l.length===0)return null;let u=e.tierTransitions.enteredHot.length,c=e.tierTransitions.exitedHot.length,p=e.tierTransitions.enteredWarm.length,a=e.tierTransitions.exitedWarm.length,f=u+c+p+a>0,g=`obscene drift (vs ${e.base}):`,w=f?`tiers: HOT +${u}/-${c} \xB7 WARM +${p}/-${a}`:null,k=[];return w&&k.push(w),k.push(...s),l.length>0&&(k.length>0&&k.push(""),k.push(...l)),`${g} | ||
| `)){let a=O(p.trim());a&&(be(a,o)||u.add(a))}let c=[...u];if(!(c.length<2||c.length>pt))for(let p=0;p<c.length;p++)for(let a=p+1;a<c.length;a++){let[f,g]=c[p]<c[a]?[c[p],c[a]]:[c[a],c[p]],b=f.includes("/")?f.slice(0,f.lastIndexOf("/")):"",k=g.includes("/")?g.slice(0,g.lastIndexOf("/")):"";if(b===k)continue;let m=`${f}\0${g}`;s.set(m,(s.get(m)??0)+1)}}return s}var re=5,se=50,ft=5;function ke(e,t,n,o={}){let i=o.minCochanges??re,s=o.minDegree??se,r=o.maxResults??ft,l=new Map;for(let[u,c]of e){if(c<i)continue;let[p,a]=u.split("\0"),f=n.has(p),g=n.has(a);if(f===g)continue;let b=t.get(p)??0,k=t.get(a)??0,m=Math.min(b,k);if(m===0)continue;let d=Math.max(b,k);if(c/d>=.9)continue;let C=Math.round(c/m*1e3)/10;if(C<s)continue;let h=f?p:a,y=f?a:p,S=l.get(h);(!S||C>S.degree)&&l.set(h,{file:h,partner:y,cochanges:c,degree:C})}return[...l.values()].sort((u,c)=>c.degree-u.degree||c.cochanges-u.cochanges||u.file.localeCompare(c.file)).slice(0,r)}function B(e,t){let n=0;for(let o of e){o.percentOfTotal=Math.round(o.score/t*1e3)/10,n+=o.score;let i=n/t;i<=it?o.tier="hot":i<=rt?o.tier="warm":o.tier="cool"}}var ae=[{key:"complexity",label:"Complexity \xD7 Churn",scoreFormula:"complexity \xD7 churn"},{key:"nesting",label:"Nesting \xD7 Churn",scoreFormula:"maxNesting \xD7 churn"},{key:"defects",label:"Fix Activity \xD7 Churn",scoreFormula:"fixes \xD7 churn"},{key:"authors",label:"Authors \xD7 Churn",scoreFormula:"authors \xD7 churn"}];function dt(e,t,n,o){let i=e.map(r=>{let l=t.get(r.file)??0,u=n(r);return{file:r.file,score:u*l,percentOfTotal:0,tier:"cool",churn:l,metricValue:u,metricDensity:o?o(r):void 0}}).filter(r=>r.score>0).sort((r,l)=>l.score-r.score),s=i.reduce((r,l)=>r+l.score,0);return s===0?[]:(B(i,s),i)}var mt=.05,gt=2;function ht(e){if(!e||e.size===0)return null;let t=0;for(let i of e.values())t+=i;if(t<gt)return null;let n=t*mt,o=0;for(let i of e.values())i<n&&o++;return o}function wt(e,t,n,o,i,s,r){let l={complexity:{extract:d=>d.complexity,density:d=>d.complexityDensity},nesting:{extract:d=>d.complexity===0?0:o.get(d.file)??0},defects:{extract:d=>n.get(d.file)??0,density:d=>{let C=n.get(d.file)??0;return d.code>0?Math.round(C/d.code*1e4)/1e4:0}},authors:{extract:d=>i.get(d.file)??0}},u={},c={},p=0;for(let d of e)d.complexity>0&&p++;c.complexity=L("filesWithComplexity",p,H.complexity,T.complexity,d=>d==="inconclusive"?`${p} files with measurable complexity \u2014 not enough to rank.`:`${p} files with measurable complexity (${d.toUpperCase()} sample size).`);let a=0;for(let d of e)d.complexity>0&&(o.get(d.file)??0)>=3&&a++;c.nesting=L("filesWithNesting>=3",a,H.nesting,T.nesting,d=>d==="inconclusive"?`${a} files with nesting depth \u2265 3 \u2014 not enough to rank.`:`${a} files with nesting depth \u2265 3 (${d.toUpperCase()} sample size).`);let f=[...n.values()].reduce((d,C)=>d+C,0),g=n.size,b=f<Q||g<Z;c.defects=L("fixCommits",f,H.defects,T.defects,d=>d==="inconclusive"||b?`${f} fix: commits across ${g} files \u2014 need \u2265 ${Q} commits across \u2265 ${Z} files (matches code-maat's --min-revs default).`:`${f} fix: commits across ${g} files (${d.toUpperCase()} sample size).`),b&&(c.defects={...c.defects,level:"inconclusive"},u.defects={reason:`insufficient data (${f} fix: commits across ${g} files, need ${Q}+ commits across ${Z}+ files)`,suggestion:"Adopt conventional commits with fix: prefix. See conventionalcommits.org",confidence:c.defects});let k=0;for(let d of i.values())d>k&&(k=d);c.authors=L("maxAuthors",k,H.authors,T.authors,d=>d==="inconclusive"?`${k} distinct authors on the most-touched file \u2014 not enough to rank ownership.`:`${k} distinct authors on the most-touched file (${d.toUpperCase()} sample size).`),k<=1&&(c.authors={...c.authors,level:"inconclusive"},u.authors={reason:"all files have the same author count \u2014 no variance to rank",confidence:c.authors});let m={};for(let d of ae){if(u[d.key])continue;if(c[d.key].level==="inconclusive"){u[d.key]={reason:c[d.key].reason,confidence:c[d.key]};continue}let C=l[d.key],h=dt(e,t,C.extract,C.density);if(h.length===0)continue;if(d.key==="authors"&&r)for(let v of h)v.minorAuthors=ht(r.get(v.file));let y=s>0?h.slice(0,s):h,S={hot:0,warm:0,cool:0};for(let v of h)S[v.tier]++;m[d.key]={label:d.label,scoreFormula:d.scoreFormula,totalScore:h.reduce((v,F)=>v+F.score,0),tierCounts:S,tiers:S,totalEntries:h.length,showing:y.length,entries:y,confidence:c[d.key]}}return{rankings:m,skipped:u}}function ce(e){let t;try{t=D(`git diff --name-only ${e}...HEAD`,{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"]})}catch{throw new Error(`Failed to compute diff against base ref '${e}'. Verify the ref exists (e.g. 'git rev-parse --verify <ref>').`)}let n=new Set;for(let o of t.toString().split(` | ||
| `)){let i=O(o.trim());i&&n.add(i)}return n}var yt=["main","master"];function Se(){for(let e of yt)try{return D(`git rev-parse --verify ${e}`,{stdio:["pipe","pipe","pipe"]}),e}catch{}}function $e(){try{return D("git rev-parse --show-toplevel",{stdio:["pipe","pipe","pipe"]}).toString().trim()||void 0}catch{return}}function bt(e,t){let n=new Map;if(t.length===0)return n;let o=t.filter(r=>et(ne(e,r)));if(o.length===0)return n;let i=ee("scc",["--by-file","--format","json","--no-cocomo","--no-gen",...o],{cwd:e,maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"]});if(i.error&&"code"in i.error&&i.error.code==="ENOENT")throw new Error("scc not found. Install it: https://github.com/boyter/scc#install");if(i.error)throw new Error(`scc spawn failed: ${i.error.message}`);if(i.status!==0)throw new Error(`scc failed on base worktree: ${i.stderr?.toString().trim()||"unknown error"}`);let s=JSON.parse(i.stdout.toString());for(let r of s)for(let l of r.Files){let u=O(l.Location);n.set(u,{file:u,code:l.Code,lines:l.Lines,complexity:l.Complexity,comments:l.Comment,complexityDensity:l.Code>0?Math.round(l.Complexity/l.Code*100)/100:0})}return n}function _(e,t){let n=tt(ne(nt(),"obscene-base-")),o={...process.env};for(let s of Object.keys(o))s.startsWith("GIT_")&&delete o[s];let i=ee("git",["worktree","add","--detach",n,e],{stdio:["pipe","pipe","pipe"],env:o});if(i.status!==0){ge(n,{recursive:!0,force:!0});let s=i.stderr?.toString().trim();throw new Error(`Could not create worktree at '${e}'${s?`: ${s}`:""}. Verify the ref exists (e.g. 'git rev-parse --verify <ref>').`)}try{return t(n)}finally{ee("git",["worktree","remove","--force",n],{stdio:["pipe","pipe","pipe"],env:o}).status!==0&&ge(n,{recursive:!0,force:!0})}}function ve(e,t,n){let o=new Map;if(t.length===0)return o;let i=_(e,s=>bt(s,t));for(let s of t){let r=n.get(s);if(r===void 0)continue;let l=i.get(s);l===void 0?o.set(s,{oldComplexity:null,newComplexity:r,change:null}):o.set(s,{oldComplexity:l.complexity,newComplexity:r,change:r-l.complexity})}return o}function le(e){let t;try{t=D("git ls-files",{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"],cwd:e})}catch{throw new Error("Not a git repository or git is not installed.")}let n=new Set;for(let o of t.toString().split(` | ||
| `)){let i=O(o.trim());i&&n.add(i)}return n}function Ee(e,t,n,o,i){let s=[];for(let[u,c]of e){if(c<o)continue;let[p,a]=u.split("\0"),f=t.get(p)??0,g=t.get(a)??0,b=Math.min(f,g),k=b>0?Math.round(c/b*1e3)/10:0,m=(n.get(p)??0)+(n.get(a)??0),d={file1:p,file2:a,cochanges:c,degree:k,totalComplexity:m,couplingScore:c,percentOfTotal:0,tier:"cool"},C=Math.max(f,g);c>0&&C>0&&c/C>=.9&&(d.lockstep=!0),i&&(i.has(p)||(d.file1Deleted=!0),i.has(a)||(d.file2Deleted=!0)),s.push(d)}s.sort((u,c)=>c.couplingScore-u.couplingScore);let r=s.reduce((u,c)=>u+c.couplingScore,0);if(r===0)return[];let l=s.map(u=>({...u,score:u.couplingScore}));B(l,r);for(let u=0;u<s.length;u++)s[u].percentOfTotal=l[u].percentOfTotal,s[u].tier=l[u].tier;return s}function De(e,t,n,o){let i=new Map,s=new Map;for(let[c,p]of e){if(p<t)continue;let[a,f]=c.split("\0");if(n){let g=Math.max(n.get(a)??0,n.get(f)??0);if(g>0&&p/g>=.9)continue}i.has(a)||i.set(a,new Set),i.has(f)||i.set(f,new Set),i.get(a)?.add(f),i.get(f)?.add(a),s.set(a,(s.get(a)??0)+p),s.set(f,(s.get(f)??0)+p)}let r=[];for(let[c,p]of i){let a={file:c,partners:p.size,strength:s.get(c),percentOfTotal:0,tier:"cool"};o&&!o.has(c)&&(a.fileDeleted=!0),r.push(a)}r.sort((c,p)=>p.strength!==c.strength?p.strength-c.strength:p.partners!==c.partners?p.partners-c.partners:c.file.localeCompare(p.file));let l=r.reduce((c,p)=>c+p.strength,0),u=r.map(c=>({...c,score:c.strength}));B(u,l);for(let c=0;c<r.length;c++)r[c].percentOfTotal=u[c].percentOfTotal,r[c].tier=u[c].tier;return r}function Ct(e,t){let n=new Map;for(let o of e){let i;try{i=we(t?ne(t,o):o,"utf-8")}catch{n.set(o,0);continue}let s=[],r=new Map,l=0;for(let a of i.split(` | ||
| `)){if(!a.trim())continue;let f=a.match(/^(\s+)/);if(!f){l=0;continue}let g=f[1];if(s.push(g),g.includes(" "))continue;let b=g.length,k=b-l;k>0&&r.set(k,(r.get(k)??0)+1),l=b}let u=4,c=0;for(let[a,f]of r)(f>c||f===c&&a<u)&&(c=f,u=a);let p=0;for(let a of s){let f=0;for(let g of a)g===" "?f+=1:g===" "&&(f+=1/u);f=Math.floor(f),f>p&&(p=f)}n.set(o,p)}return n}var xt=[{dir:".github",pattern:".github/**",comment:"GitHub Actions and workflows"},{dir:".circleci",pattern:".circleci/**",comment:"CircleCI configuration"},{dir:".husky",pattern:".husky/**",comment:"Git hooks"},{dir:".vscode",pattern:".vscode/**",comment:"VS Code settings"},{dir:".idea",pattern:".idea/**",comment:"JetBrains settings"},{dir:"scripts",pattern:"scripts/**",comment:"Build and utility scripts"},{dir:"docs",pattern:"docs/**",comment:"Documentation"},{dir:"docker",pattern:"docker/**",comment:"Docker configuration"},{dir:"fixtures",pattern:"fixtures/**",comment:"Test fixtures"},{dir:"vendor",pattern:"vendor/**",comment:"Vendored dependencies"}],kt=[{test:/\.generated\./,pattern:"*.generated.*",comment:"Generated code"},{test:/\.gen\.[^.]+$/,pattern:"*.gen.*",comment:"Generated code"},{test:/\.config\.\w/,pattern:"*.config.*",comment:"Configuration files"},{test:/(?:^|\/)\.gitlab-ci/,pattern:".gitlab-ci*",comment:"GitLab CI configuration"},{test:/^\.claude\/commands\//,pattern:".claude/commands/**",comment:"Claude Code slash commands (often generated from sources)"},{test:/^\.opencode\/commands\//,pattern:".opencode/commands/**",comment:"OpenCode slash commands (often generated from sources)"},{test:/^\.cursor\/rules\//,pattern:".cursor/rules/**",comment:"Cursor rules (often generated from sources)"}];function Re(){let e=le(),t=[],n=new Set;for(let o of e){let i=o.indexOf("/");i>0&&n.add(o.slice(0,i))}for(let o of xt)n.has(o.dir)&&t.push({pattern:o.pattern,comment:o.comment});for(let o of kt)for(let i of e)if(o.test.test(i)){t.push({pattern:o.pattern,comment:o.comment});break}return t}function Oe(e,t=oe){let n=["# Generated by obscene init","# Edit this file to customize which files are excluded from analysis.","# Patterns use glob syntax (same as .gitignore).","# See: https://github.com/wbern/obscene#ignore-files",""];for(let o of t){n.push(`# ${o.title}`);for(let i of o.patterns)n.push(i.pattern);n.push("")}if(e.length>0){n.push("# Project-specific patterns");for(let o of e)n.push(`# ${o.comment}`),n.push(o.pattern);n.push("")}return n.join(` | ||
| `)}var St=10,he={inconclusive:0,weak:1,plausible:2,acceptable:3};function $t(e){let t=Object.values(e).map(i=>i.confidence),n=t.length;if(n<2)return{level:"inconclusive",reason:`${n} input ranking \u2014 RRF requires \u2265 2 independent rankings.`,inputs:{metric:"inputRankings",value:n,thresholds:{weak:2,plausible:3,acceptable:4}},source:T.composite};let o="acceptable";for(let i of t)he[i.level]<he[o]&&(o=i.level);return{level:o,reason:`Composite inherits min-of-inputs across ${n} rankings (weakest: ${o.toUpperCase()}).`,inputs:{metric:"inputRankings",value:n,thresholds:{weak:2,plausible:3,acceptable:4}},source:T.composite}}function vt(e,t,n){let o=Object.keys(e).length,i=$t(e),s=new Map;for(let p of Object.values(e))for(let a=0;a<p.entries.length;a++){let f=p.entries[a].file,g=1/(St+a+1),b=s.get(f);b?(b.score+=g,b.dims+=1):s.set(f,{score:g,dims:1})}let r=[];for(let[p,a]of s)r.push({file:p,score:Math.round(a.score*1e4)/1e4,percentOfTotal:0,tier:"cool",churn:t.get(p)??0,dimensionCount:a.dims});r.sort((p,a)=>a.score-p.score);let l=r.reduce((p,a)=>p+a.score,0);if(l===0)return{label:"Combined",scoreFormula:"reciprocal rank fusion across all dimensions",totalScore:0,tierCounts:{hot:0,warm:0,cool:0},tiers:{hot:0,warm:0,cool:0},totalDimensions:o,totalEntries:0,showing:0,entries:[],confidence:i};B(r,l);let u=n>0?r.slice(0,n):r,c={hot:0,warm:0,cool:0};for(let p of r)c[p.tier]++;return{label:"Combined",scoreFormula:"reciprocal rank fusion across all dimensions",totalScore:Math.round(l*1e4)/1e4,tierCounts:c,tiers:c,totalDimensions:o,totalEntries:r.length,showing:u.length,entries:u,confidence:i}}function A(e,t,n,o,i="commits"){let s=i==="lines"?st(t,o):j(t,o),r=ct(t,o),l=ut(t,o),u=at(t,o),c=new Map;for(let[h,y]of l)c.set(h,y.size);let p=Ct(e.map(h=>h.file),o),{rankings:a,skipped:f}=wt(e,s,r,p,c,n,l),g=vt(a,s,n),b=0;for(let h of e)b+=h.complexity;let k=t*ue,m=new Map(e.map(h=>[h.file,h])),d=[];for(let[h,y]of u){let S=m.get(h);S&&d.push({file:h,dormancyDays:y.dormancyDays,dormancyMultiple:y.dormancyMultiple,lastTouchedBeforeWindow:y.lastTouchedBeforeWindow,firstTouchedInWindow:y.firstTouchedInWindow,complexity:S.complexity,churn:s.get(h)??0})}d.sort((h,y)=>y.dormancyMultiple-h.dormancyMultiple||h.file.localeCompare(y.file));let C={windowDays:k,minDormancyMultiple:te,minDormancyDays:k*te,entries:d};return{rankings:a,skipped:f,composite:g,corpus:{fileCount:e.length,totalComplexity:b},churn:s,reawakened:C}}function Me(e,t){if(t<=0)return e;let n={};for(let[i,s]of Object.entries(e.rankings)){let r=s.entries.slice(0,t);n[i]={...s,entries:r,showing:r.length}}let o=e.composite.entries.slice(0,t);return{rankings:n,skipped:e.skipped,composite:{...e.composite,entries:o,showing:o.length},corpus:e.corpus,churn:e.churn,reawakened:e.reawakened}}function U(e){let t=N(e.excludes,e.cwd),n=A(t,e.months,0,e.cwd,e.churnMode);return{files:t,rankings:n.rankings,skipped:n.skipped,composite:n.composite,reawakened:n.reawakened,corpus:n.corpus}}function G(e,t,n,o){let i=new Map;for(let m of n.composite.entries)i.set(m.file,{score:m.score,tier:m.tier});let s=new Map;for(let m of o.composite.entries)s.set(m.file,{score:m.score,tier:m.tier});let r=new Set(n.files.map(m=>m.file)),l=new Set(o.files.map(m=>m.file)),u=[],c=[];for(let m of l)r.has(m)||u.push(m);for(let m of r)l.has(m)||c.push(m);u.sort(),c.sort();let p=new Set([...i.keys(),...s.keys()]),a=[],f=[],g=[],b=[],k=[];for(let m of p){let d=i.get(m),C=s.get(m),h=d?.score??null,y=C?.score??null,S=d?.tier??null,v=C?.tier??null,F=h!==null&&y!==null?y-h:null,Ze=F!==null&&h!==null&&h!==0?Math.round(F/h*1e3)/10:null,E;S===null?E="new":v===null?E="deleted":S!=="hot"&&v==="hot"?E="entered-hot":S==="cool"&&v==="warm"?E="entered-warm":S==="hot"&&v!=="hot"?E="exited-hot":S==="warm"&&v==="cool"?E="exited-warm":E="stable",E==="entered-hot"?f.push(m):E==="entered-warm"?g.push(m):E==="exited-hot"?b.push(m):E==="exited-warm"&&k.push(m),a.push({file:m,oldScore:h,newScore:y,change:F!==null?Math.round(F*1e4)/1e4:null,percentChange:Ze,oldTier:S,newTier:v,transition:E})}return a.sort((m,d)=>{let C=Math.abs(m.change??0),h=Math.abs(d.change??0);return C!==h?h-C:m.file.localeCompare(d.file)}),f.sort(),g.sort(),b.sort(),k.sort(),{base:e,head:t,newFiles:u,deletedFiles:c,tierTransitions:{enteredHot:f,enteredWarm:g,exitedHot:b,exitedWarm:k},scoreChanges:a,perDimensionDeltas:{complexity:{oldTotal:n.corpus.totalComplexity,newTotal:o.corpus.totalComplexity,change:o.corpus.totalComplexity-n.corpus.totalComplexity},fileCount:{oldTotal:n.corpus.fileCount,newTotal:o.corpus.fileCount,change:o.corpus.fileCount-n.corpus.fileCount}}}}function Te(e){return L("commitsInWindow",e,H.coupling,T.coupling,t=>t==="inconclusive"?`${e} commits in window \u2014 need \u2265 ${H.coupling.weak} (matches code-maat's --min-revs default).`:`${e} commits in window (${t.toUpperCase()} sample size).`)}function Ie(e,t){try{let n=D(`git rev-list --count --since="${e} months ago" HEAD`,{stdio:["pipe","pipe","pipe"],cwd:t});return parseInt(n.toString().trim(),10)||0}catch{throw new Error("Not a git repository or git is not installed.")}}var ue=30;function Fe(e,t){let n=e*ue,o;try{let l=D("git log --format=%ct --reverse HEAD",{maxBuffer:52428800,stdio:["pipe","pipe","pipe"],cwd:t}).toString().split(` | ||
| `,1)[0].trim();if(o=parseInt(l,10),!Number.isFinite(o)||o<=0)return{windowDays:n,spanDays:0,underCovered:!0}}catch{throw new Error("Not a git repository or git is not installed.")}let i=Math.floor(Date.now()/1e3),s=Math.max(0,Math.floor((i-o)/86400));return{windowDays:n,spanDays:s,underCovered:s<n}}import x from"picocolors";import M from"picocolors";var Et=/\x1b\[[0-9;]*m/g;function Dt(e){return e>=11904&&e<=12543||e>=12800&&e<=13311||e>=13312&&e<=40959||e>=44032&&e<=55215||e>=63744&&e<=64255||e>=65281&&e<=65376||e>=65504&&e<=65510||e>=9728&&e<=9983||e>=127744&&e<=129791||e>=131072&&e<=195103}function He(e){let t=e.replace(Et,""),n=0;for(let o of t){let i=o.codePointAt(0);i===65038||i===65039||(n+=Dt(i)?2:1)}return n}function $(e,t){let n=He(e);return n>=t?e:e+" ".repeat(t-n)}function w(e,t){let n=He(e);return n>=t?e:" ".repeat(t-n)+e}function R(e,t){if(t<=0)return"";if(e.length<=t)return e;if(t===1)return"\u2026";let n=t-1,o=Math.ceil(n*.6),i=n-o;return`${e.slice(0,i)}\u2026${e.slice(e.length-o)}`}function W(e){return e==="hot"?M.red("\u{1F525} HOT "):e==="warm"?M.yellow("\u2600\uFE0F WARM"):M.blue("\u{1F9CA} COOL")}function P(e,t){return e==="hot"?M.red(t):e==="warm"?M.yellow(t):M.blue(t)}function z(e,t,n){let o=[];return o.push(`Tiers: ${M.red(`${e.hot} HOT`)}, ${M.yellow(`${e.warm} WARM`)}, ${M.blue(`${e.cool} COOL`)}`),o.push(`Showing: ${t} of ${n}`),o}var Rt={inconclusive:x.gray,weak:x.yellow,plausible:x.cyan,acceptable:x.green};function V(e){let t=Rt[e.level];return[t(`Confidence: ${e.level.toUpperCase()} \u2014 ${e.reason}`)]}var Ot=Object.fromEntries(ae.map(e=>[e.key,e.label]));function Ne(e){let t=[],{summary:n,files:o}=e;t.push(`Complexity Report \u2014 ${n.fileCount} files, ${n.totalComplexity} total complexity`),t.push(`Showing: ${n.showing} | Avg complexity/file: ${n.avgComplexityPerFile}`),t.push(""),t.push($("File",60)+w("Code",8)+w("Complexity",12)+w("Density",9)+w("Comments",10)),t.push("\u2500".repeat(99));for(let i of o)t.push($(R(i.file,58),60)+w(String(i.code),8)+w(String(i.complexity),12)+w(i.complexityDensity.toFixed(2),9)+w(String(i.comments),10));return t.push(""),t.push(x.dim("Complexity=cyclomatic branch/loop count | Density=complexity/code | Comments=comment lines")),t.push(x.dim("High complexity is expected for parsers, state machines, and business logic. Compare density across files, not raw values.")),t.push(x.dim("Docs: https://github.com/wbern/obscene#metrics")),t.join(` | ||
| `)}function Ae(e){return e?e.oldComplexity===null||e.change===null?"new":e.change===0?"0":e.change>0?`+${e.change}`:String(e.change):"\xB7"}function Mt(e,t){let n=[{header:"File",width:50,align:"left",value:r=>R(r.file,48)},{header:"Score",width:8,align:"right",value:r=>r.score.toLocaleString()},{header:"%",width:7,align:"right",value:r=>r.percentOfTotal.toFixed(1)},{header:"Churn",width:7,align:"right",value:r=>String(r.churn)}],o={complexity:[{header:"Cmplx",width:7,align:"right",value:r=>String(r.metricValue)},{header:"Dens",width:7,align:"right",value:r=>(r.metricDensity??0).toFixed(2)}],nesting:[{header:"Nest",width:6,align:"right",value:r=>String(r.metricValue)}],defects:[{header:"Fixes",width:6,align:"right",value:r=>String(r.metricValue)},{header:"FxDns",width:7,align:"right",value:r=>(r.metricDensity??0).toFixed(4)}],authors:[{header:"Auth",width:6,align:"right",value:r=>String(r.metricValue)},{header:"MinAuth",width:9,align:"right",value:r=>r.minorAuthors===null||r.minorAuthors===void 0?"\u2014":String(r.minorAuthors)}]},i={header:"Tier",width:12,align:"right",value:r=>W(r.tier)},s={header:"\u0394",width:7,align:"right",value:r=>Ae(r.complexityDelta)};return[...n,...o[e]??[],...t?[s]:[],i]}var Tt={complexity:"\u{1F9EC}",nesting:"\u{1F4CF}",defects:"\u{1F527}",authors:"\u{1F465}"};function It(e,t,n,o){let i=[],s=Mt(e,o),r=Tt[e],l=r?`${r} `:"",u=t.label.toUpperCase().replace("CHURN","\u{1F504} CHURN");if(i.push(`${l}${u} \u2014 Total score: ${t.totalScore.toLocaleString()}`),i.push(...V(t.confidence)),n)for(let a of n.split(` | ||
| `))i.push(x.dim(a));i.push(...z(t.tierCounts,t.showing,t.totalEntries)),i.push("");let c=s.map(a=>a.align==="left"?$(a.header,a.width):w(a.header,a.width)).join("");i.push(c);let p=s.reduce((a,f)=>a+f.width,0);i.push("\u2500".repeat(p));for(let a of t.entries){let g=s.map(b=>{let k=b.value(a);return b.align==="left"?$(k,b.width):w(k,b.width)}).join("");i.push(P(a.tier,g))}return i}function Ft(e){let t=[];t.push(""),t.push(x.cyan(`Full Delta \u2014 ${e.base} \u2192 ${e.head}`)),t.push(x.dim("Tier transitions are relative percentile bands \u2014 a file can shift tiers because its absolute score moved OR because the rest of the corpus moved around it. scoreChanges carries the absolute delta."));let{enteredHot:n,enteredWarm:o,exitedHot:i,exitedWarm:s}=e.tierTransitions;if(n.length===0&&o.length===0&&i.length===0&&s.length===0&&e.newFiles.length===0&&e.deletedFiles.length===0)t.push(x.dim("No tier transitions, no new/deleted files."));else{let p=(a,f)=>{if(f.length!==0){t.push(a);for(let g of f.slice(0,10))t.push(` ${R(g,80)}`);f.length>10&&t.push(x.dim(` \u2026 and ${f.length-10} more`))}};p(x.red(` \u2191 entered HOT (${n.length}):`),n),p(x.yellow(` \u2191 entered WARM (${o.length}):`),o),p(x.green(` \u2193 cooled out of HOT (${i.length}):`),i),p(x.green(` \u2193 cooled out of WARM (${s.length}):`),s),p(x.cyan(` + new files (${e.newFiles.length}):`),e.newFiles),p(x.cyan(` \u2212 deleted files (${e.deletedFiles.length}):`),e.deletedFiles)}let r=e.perDimensionDeltas.complexity,l=e.perDimensionDeltas.fileCount,u=r.change>0?"+":"",c=l.change>0?"+":"";return t.push(""),t.push(x.dim(`Corpus: complexity ${r.oldTotal} \u2192 ${r.newTotal} (${u}${r.change}) \xB7 files ${l.oldTotal} \u2192 ${l.newTotal} (${c}${l.change})`)),t}function Ht(e){let t=[],{windowDays:n,minDormancyMultiple:o,minDormancyDays:i,entries:s}=e;t.push(x.magenta(`Reawakened \u2014 dormant \u2265 ${o}\xD7 window (${i}d) then touched again`)),t.push(x.dim(` Window: ${n}d \xB7 Rule: gap between last pre-window commit and first in-window commit \u2265 ${i}d`)),t.push("");let r=` ${$("File",40)} ${w("Dormancy",12)} ${w("\xD7Window",10)} ${w("Cx",6)} ${w("Churn",6)}`;t.push(x.dim(r));for(let l of s)t.push(` ${$(R(l.file,40),40)} ${w(`${l.dormancyDays}d`,12)} ${w(`${l.dormancyMultiple}\xD7`,10)} ${w(String(l.complexity),6)} ${w(String(l.churn),6)}`);return t}function q(e){let t=[],{churnWindow:n,churnMode:o,rankings:i,corpus:s,delta:r,fullDelta:l}=e;if(r&&(t.push(x.cyan(`Delta \u2014 ${r.changedFiles.length} file${r.changedFiles.length===1?"":"s"} changed since ${r.base}`)),r.fallback&&t.push(x.yellow(` \u26A0 full-delta unavailable \u2014 showing filtered view. Reason: ${r.fallback.reason}`)),r.changedFiles.length===0))return t.push(""),t.push(x.dim("No changes \u2014 nothing to rank.")),t.join(` | ||
| `);l&&t.push(...Ft(l));let u=o==="lines"?" (line-based)":"";t.push(`Hotspots \u2014 ${n} churn window${u}`),s&&s.fileCount>0&&s.totalComplexity===0&&(t.push(""),t.push(x.yellow("Note: no measurable code complexity detected across this corpus (cyclomatic = 0).")),t.push(x.yellow("Rankings reflect size and churn only \u2014 HOT/WARM/COOL are relative groupings, not risk labels."))),t.push("");let c=r!==void 0&&Object.values(i).some(f=>f.entries.some(g=>g.complexityDelta!==void 0)),p=Object.keys(i);for(let f=0;f<p.length;f++){let g=p[f];t.push(...It(g,i[g],e.guide[g],c)),f<p.length-1&&(t.push(""),t.push("\xB7 \xB7 \xB7"),t.push(""))}if(e.skipped)for(let[f,g]of Object.entries(e.skipped)){t.push("");let b=Ot[f]??`${f.charAt(0).toUpperCase()+f.slice(1)} \xD7 Churn`;t.push(`${b} \u2014 skipped (${g.reason})`),g.suggestion&&t.push(` ${g.suggestion}`)}e.reawakened&&(t.push(""),t.push("\xB7 \xB7 \xB7"),t.push(""),t.push(...Ht(e.reawakened))),t.push(""),t.push(x.dim("Score=metric\xD7churn | Tiers are relative to THIS codebase, not absolute quality grades."));let a=s!==void 0&&s.fileCount>0&&s.totalComplexity===0;return t.push(x.dim(a?"High scores flag files that change often and are sizable \u2014 neither is bad in itself.":"High scores flag review candidates, not bad code \u2014 stable complex files (parsers, engines) score high naturally.")),t.push(x.dim("Docs: https://github.com/wbern/obscene#metrics")),s?.filtered===!1&&(t.push(""),t.push(x.yellow("\u26A0 Corpus unfiltered \u2014 no .obsignore found. Lockfiles, generated code, and vendored dependencies may dominate these rankings.")),t.push(x.dim(" Run `obscene init` to generate a starter .obsignore."))),t.join(` | ||
| `)}function Le(e){let t=[],{tierCounts:n,totalScore:o,churnWindow:i,couplings:s}=e;t.push(`Coupling \u2014 ${i} churn window | Min shared: ${e.minCochanges} | Total score: ${o.toLocaleString()}`),t.push(...V(e.confidence)),t.push(...z(n,e.showing,e.totalCouplings)),t.push($("File 1",35)+$("File 2",35)+w("Shared",7)+w("Degree",8)+w("Cmplx",7)+w("Tier",12)),t.push("\u2500".repeat(104));let r=!1,l=!1;for(let u of s){(u.file1Deleted||u.file2Deleted)&&(r=!0),u.lockstep&&(l=!0);let c=u.file1Deleted?`\u2020 ${R(u.file1,31)}`:R(u.file1,33),p=u.file2Deleted?`\u2020 ${R(u.file2,31)}`:R(u.file2,33),a=u.lockstep?`${u.degree.toFixed(1)}\u21C4`:`${u.degree.toFixed(1)}%`,f=$(c,35)+$(p,35)+w(String(u.cochanges),7)+w(a,8)+w(String(u.totalComplexity),7)+w(W(u.tier),12);t.push(P(u.tier,f))}return t.push(""),t.push(x.dim("Shared=co-changed commits | Degree=shared/min(churn)\xD7100 | Cmplx=sum of both files")),r&&t.push(x.dim("\u2020 = file no longer present at HEAD (deleted or renamed)")),l&&t.push(x.dim("\u21C4 = lockstep pair (both files only ever changed together \u2014 signal is real but uninformative)")),t.push(x.dim("Tiers are relative to THIS codebase, not absolute quality grades. High coupling may be intentional and fine.")),t.push(x.dim("Same-directory pairs excluded. Commits touching >20 files skipped. Only cross-directory dependencies shown.")),t.push(x.dim("Docs: https://github.com/wbern/obscene#metrics")),e.sumOfCoupling&&e.sumOfCoupling.length>0&&(t.push(""),t.push(...Nt(e.sumOfCoupling,e.confidence))),t.join(` | ||
| `)}function Nt(e,t){let n=[];n.push("\u2500".repeat(68)),n.push(`${x.bold("Sum of Coupling")} ${x.dim("(experimental \u2014 not independently validated)")} \u2014 files whose couplings concentrate the most cross-dir change traffic`),n.push(...V(t)),n.push(""),n.push($("File",40)+w("Partners",10)+w("Strength",10)+w("Tier",8)),n.push("\u2500".repeat(68));let o=!1;for(let i of e){let s=i.fileDeleted?`\u2020 ${i.file}`:i.file;i.fileDeleted&&(o=!0);let r=$(R(s,38),40)+w(String(i.partners),10)+w(String(i.strength),10)+w(W(i.tier),8);n.push(P(i.tier,r))}return n.push(""),o&&n.push(x.dim("\u2020 = file no longer present at HEAD (deleted or renamed away); coupling signal is historical.")),n.push(x.dim("Partners=distinct cross-dir co-change partners | Strength=\u03A3 pair cochange counts (= code-maat's SoC analysis, filtered to cross-dir pairs and \u226420-file commits).")),n.push(x.dim(`Navigation aid: high strength means "worth a look at this file's couplings", not "this file is defect-prone".`)),n.push(x.dim("EXPERIMENTAL: NOT independently validated against defect data; may change, be reframed, or be removed.")),n}function _e(e){return e==="hot"?"HOT ":e==="warm"?"WARM":"COOL"}function We(e){let t=e.historyCoverage;return t?.underCovered?` [history covers ~${t.spanDays}d, window ${t.windowDays}d]`:""}function At(e){return`${e} commit${e===1?"":"s"}`}function J(e){let t=[],n=e.composite,o=e.corpus,i=o?`${o.fileCount} files, ${o.totalComplexity} total complexity`:"",s=`Hotspot landscape (composite RRF, ${e.churnWindow} window`+(i?`, ${i}`:"")+We(e)+"):";if(t.push(s),!n||n.entries.length===0)return t.push("(no composite ranking \u2014 insufficient signal)"),t.join(` | ||
| `);t.push(`Confidence: ${n.confidence.level.toUpperCase()} \u2014 ${n.confidence.reason}`);for(let r of n.entries)t.push(`${_e(r.tier)} ${$(r.file,50)} ${w(`${r.percentOfTotal.toFixed(1)}%`,6)} ${w(At(r.churn),12)} ${r.dimensionCount}/${n.totalDimensions} dims`);return t.push(""),t.push("For volume-weighted churn: --churn-mode lines. For co-change pairs: obscene coupling."),t.join(` | ||
| `)}function Pe(e){let t=[],{summary:n,files:o}=e;t.push(`Complexity report \u2014 ${n.fileCount} files, ${n.totalComplexity} total complexity, ${n.avgComplexityPerFile} avg/file (showing ${n.showing}):`);for(let i of o)t.push(`${$(i.file,50)} complexity=${w(String(i.complexity),5)} density=${i.complexityDensity.toFixed(2)} code=${i.code}`);return t.join(` | ||
| `)}function je(e){let t=[],n=`Coupling \u2014 ${e.churnWindow} window, min shared: ${e.minCochanges}`+We(e)+":";if(t.push(n),e.couplings.length===0)return t.push("(no pairs above thresholds)"),t.join(` | ||
| `);t.push(`Confidence: ${e.confidence.level.toUpperCase()} \u2014 ${e.confidence.reason}`);for(let o of e.couplings){let i=`${o.file1} \u2194 ${o.file2}`,s=o.lockstep?"\u21C4":"%";t.push(`${_e(o.tier)} ${$(i,70)} ${w(`${o.cochanges} shared`,10)} ${w(`${o.degree.toFixed(1)}${s}`,8)}`)}return t.join(` | ||
| `)}function Be(e){let t=[],n=e.entries.some(s=>s.complexityDelta!==void 0),o=n?91:84;t.push("\u2550".repeat(o)),t.push(`\u2605 ${e.label.toUpperCase()} \u2014 Total score: ${e.totalScore.toLocaleString()}`),t.push(...V(e.confidence)),t.push(...z(e.tierCounts,e.showing,e.totalEntries)),t.push("");let i=$("File",50)+w("Score",9)+w("Churn",7)+w("Dims",6);n&&(i+=w("\u0394",7)),i+=w("Tier",12),t.push(i),t.push("\u2500".repeat(o));for(let s of e.entries){let r=$(R(s.file,48),50)+w(s.score.toFixed(4),9)+w(String(s.churn),7)+w(`${s.dimensionCount}/${e.totalDimensions}`,6);n&&(r+=w(Ae(s.complexityDelta),7)),r+=w(W(s.tier),12),t.push(P(s.tier,r))}return t.join(` | ||
| `)}var Lt=25,_t=new Set(["SessionStart","Setup","SubagentStart","UserPromptSubmit","UserPromptExpansion","PreToolUse","PostToolUse","PostToolUseFailure","PostToolBatch"]);function Ue(e){return`${e>0?"+":""}${e.toFixed(0)}%`}function Ge(e,t={}){let n=t.significantPercentChange??Lt,o=new Set([...e.tierTransitions.enteredHot,...e.tierTransitions.enteredWarm,...e.tierTransitions.exitedHot,...e.tierTransitions.exitedWarm]),i=new Map(e.scoreChanges.map(m=>[m.file,m])),s=[];for(let m of[...o].sort()){let d=i.get(m);if(!d)continue;let C=d.oldTier??"\u2014",h=d.newTier??"\u2014",y=d.percentChange!==null?Ue(d.percentChange):"\u2014";s.push(`- ${m}: ${C} \u2192 ${h} (score ${y})`)}let r=e.scoreChanges.filter(m=>!o.has(m.file)).filter(m=>m.percentChange!==null&&Math.abs(m.percentChange)>=n).sort((m,d)=>Math.abs(d.percentChange)-Math.abs(m.percentChange));for(let m of r){let d=m.newTier??m.oldTier??"\u2014";s.push(`- ${m.file}: score ${Ue(m.percentChange)} (stayed ${d})`)}let l=Wt(t.reminders,t.reminderMinDegree);if(s.length===0&&l.length===0)return null;let u=e.tierTransitions.enteredHot.length,c=e.tierTransitions.exitedHot.length,p=e.tierTransitions.enteredWarm.length,a=e.tierTransitions.exitedWarm.length,f=u+c+p+a>0,g=`obscene drift (vs ${e.base}):`,b=f?`tiers: HOT +${u}/-${c} \xB7 WARM +${p}/-${a}`:null,k=[];return b&&k.push(b),k.push(...s),l.length>0&&(k.length>0&&k.push(""),k.push(...l)),`${g} | ||
| ${k.join(` | ||
| `)}`}function _t(e,t){if(!e||e.length===0)return[];let o=[`co-change reminders (\u2265${te} commits, \u2265${t??ne}% degree):`];for(let i of e)o.push(`- ${i.file} \u2194 ${i.partner}: ${i.cochanges} shared commits (degree ${i.degree.toFixed(0)}%)`);return o.push("ignore if unrelated to this change."),o}function Ge(e,t){return Lt.has(t)?{hookSpecificOutput:{hookEventName:t,additionalContext:e}}:{systemMessage:e}}var I=new Bt;I.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("2.13.0");var Ut={complexity:"Cyclomatic complexity (branch/loop count). NOT a quality judgment \u2014 a 500-line parser will naturally score high. Compare density, not raw values.",complexityDensity:"Complexity per line of code. Normalizes for file size. >0.25 suggests dense logic worth reviewing; <0.10 is typical for straightforward code.",comments:"Comment line count. Low comments in high-density files may indicate under-documented logic. High comments alone is not a problem."},qe={rankings:"Four independent ranking tables, each scoring files by a different metric \xD7 churn. A file may rank high in one dimension but not others.",complexity:`complexity \xD7 churn. Complex code that changes often poses maintenance risk. | ||
| `)}`}function Wt(e,t){if(!e||e.length===0)return[];let o=[`co-change reminders (\u2265${re} commits, \u2265${t??se}% degree):`];for(let i of e)o.push(`- ${i.file} \u2194 ${i.partner}: ${i.cochanges} shared commits (degree ${i.degree.toFixed(0)}%)`);return o.push("ignore if unrelated to this change."),o}function ze(e,t){return _t.has(t)?{hookSpecificOutput:{hookEventName:t,additionalContext:e}}:{systemMessage:e}}var I=new Bt;I.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("2.14.0");var Ut={complexity:"Cyclomatic complexity (branch/loop count). NOT a quality judgment \u2014 a 500-line parser will naturally score high. Compare density, not raw values.",complexityDensity:"Complexity per line of code. Normalizes for file size. >0.25 suggests dense logic worth reviewing; <0.10 is typical for straightforward code.",comments:"Comment line count. Low comments in high-density files may indicate under-documented logic. High comments alone is not a problem."},pe={rankings:"Four independent ranking tables, each scoring files by a different metric \xD7 churn. A file may rank high in one dimension but not others.",complexity:`complexity \xD7 churn. Complex code that changes often poses maintenance risk. | ||
| Metric concept: McCabe cyclomatic complexity (1976) via scc \xB7 Strength: objective, language-agnostic \xB7 Limit: parsers and state machines score high naturally`,nesting:`maxNesting \xD7 churn. Deeply nested code that changes often is harder to reason about. | ||
@@ -33,27 +33,33 @@ Metric concept: cognitive complexity research (SonarSource, G. Ann Campbell 2018) \xB7 Strength: catches hard-to-follow control flow \xB7 Limit: some patterns (error chains, config) legitimately nest deep`,defects:`fixes \xD7 churn. Count of fix: commits touching the file \xD7 churn. High values can mean latent fragility, but they also flag features that got debugged thoroughly \u2014 read the fix-commit history before concluding which. | ||
| Metric concept: code ownership research (Bird et al. 2011, Microsoft); Co-authored-by trailers folded into author set to close the squash-merge gap \xB7 Strength: flags diffuse ownership risk \xB7 Limit: doesn't measure expertise depth, bot authors filtered automatically`,composite:`Combined ranking using Reciprocal Rank Fusion (RRF) across all dimensions. Files appearing near the top of multiple rankings score highest. | ||
| Metric concept: RRF (Cormack et al. 2009) \xB7 Strength: robust to outliers, no normalization needed \xB7 Limit: equal weight across all dimensions`,tier:"Relative ranking within THIS codebase (top 50% = hot, next 30% = warm, bottom 20% = cool). NOT an absolute quality grade \u2014 a hot file is under heavy load, not necessarily broken.",corpus:"Aggregate stats for the analyzed file set (post-exclude \u2014 files filtered by .obsignore or --exclude are not counted). When totalComplexity is 0, the rankings reflect size and churn only; HOT/WARM/COOL become relative groupings rather than risk labels.",confidence:"Epistemic stamp on each ranking \u2014 INCONCLUSIVE / WEAK / PLAUSIBLE / ACCEPTABLE. These are engineering-judgment sample-size tiers, with the weak floor for defects matching code-maat's --min-revs default of 5. ACCEPTABLE is the ceiling \u2014 the tool never claims certainty about code quality, only that the sample supports the ranking. INCONCLUSIVE rankings are surfaced under skipped rather than ranked.",reawakened:"Files that were dormant for \u2265 3\xD7 the churn window and just got touched again inside it. The objective rule: gap between the latest pre-window commit and the earliest in-window commit must be \u2265 minDormancyDays. Reawakened code often carries forgotten context \u2014 the original author may be gone, mental models stale.\nMetric concept: forensic 'reawakened files' signal from Tornhill, *Your Code as a Crime Scene* (2nd ed., Ch. 2) \xB7 Strength: highlights risk that pure churn \xD7 complexity misses \xB7 Limit: pre-window history must exist (omits truly new files), and `git log --follow` isn't used so file renames break the dormancy chain."},Gt={cochanges:"Times both files appeared in the same commit. Higher values suggest a dependency between the files. Same-directory pairs are excluded \u2014 only cross-directory pairs are shown.",degree:"Percentage: shared commits / min(churn of file1, file2) \xD7 100. Shows how tightly coupled the pair is relative to their individual change rates. 100% means every change to the less-active file also touched the other.",totalComplexity:"Sum of both files' cyclomatic complexity. Highlights coupled pairs where the involved code is also complex \u2014 hidden dependency + high complexity compounds maintenance risk.",tier:"Relative ranking within THIS codebase's coupling pairs (top 50% = hot, next 30% = warm, bottom 20% = cool). NOT an absolute quality grade. 'hot' means this pair co-changes more than most \u2014 it may be intentional and fine.",deleted:"file1Deleted / file2Deleted are set when the file is no longer present at HEAD (deleted or renamed away). The coupling signal is historical \u2014 the pair is not actionable in the current tree.",lockstep:"Set when shared commits / max(churn) \u2265 0.9 \u2014 both files almost always change together over the window. Typical of generator/mirror pairs (README \u2194 src/README, *.pb.go \u2194 *.proto). The coupling signal is real but uninformative; treat the pair as a single unit from git's perspective.",confidence:"Epistemic stamp on the coupling table \u2014 INCONCLUSIVE / WEAK / PLAUSIBLE / ACCEPTABLE. Tied to the number of commits in the analysis window. The weak floor of 5 matches code-maat's --min-revs default (Adam Tornhill); higher tiers are engineering judgment. ACCEPTABLE means the sample supports the ranking; it never asserts the couplings themselves are bad.",sumOfCoupling:'Per-file Sum of Coupling. `strength` = \u03A3 pair cochange counts a file participates in \u2014 equivalent to the SoC analysis in code-maat (\u03A3(changeset_size \u2212 1); see github.com/adamtornhill/code-maat), restricted here to cross-directory pairs and commits touching \u226420 files; near-lockstep pairs (count/max(churn) \u2265 0.9) are suppressed so mirror/generator artifacts don\'t dominate. `partners` = distinct co-change partners (graph degree). Treat as a navigation aid \u2014 high `strength` says "this file\'s couplings deserve a closer look", not "this file is buggy". EXPERIMENTAL: this surface has NOT been independently validated against defect data; it may change, be reframed, or be removed.'},Je=["json","table","compact"];function zt(e){if(!Je.includes(e))throw new Ke(`must be one of: ${Je.join(", ")}`);return e}function Vt(e){let t=Number.parseFloat(e);if(!Number.isFinite(t)||t<0||t>100)throw new Ke("must be a number between 0 and 100");return t}function pe(e){return e.option("--top <n>","limit to top N entries (0 = all)","20").option("--format <type>","output format: json | table | compact (compact = terse plain-text lines for hooks and quick reads)",zt,"json").option("--exclude <patterns...>","additional file patterns to exclude (also reads .obsignore / .obsceneignore)")}pe(I.command("report").description("per-file complexity data")).action(e=>{try{qt(e)}catch(t){q(t)}});pe(I.command("hotspots",{isDefault:!0}).description("churn \xD7 complexity hotspot analysis (default)")).option("--months <n>","churn window in months","3").option("--base [ref]","delta mode: filter rankings to files changed since this ref (bare flag auto-detects main/master)").option("--full-delta","with --base, run the full hotspots pipeline against the base ref too and emit a structured before/after diff (slower; tier transitions, score deltas, new/deleted files)").option("--paths <files...>","filter displayed entries to these paths (tiers stay corpus-anchored \u2014 answers 'are MY changes in hot territory?')").option("--since <ref>","shorthand for --paths $(git diff --name-only <ref>...HEAD) \u2014 filter to files changed since ref").option("--churn-mode <mode>","how to count churn: 'commits' counts commits touching each file; 'lines' sums added+deleted lines via git log --numstat","commits").action(e=>{try{Zt(e)}catch(t){q(t)}});pe(I.command("coupling").description("temporal coupling \u2014 files that change together across directories")).option("--months <n>","churn window in months","3").option("--min-cochanges <n>","minimum shared commits to include","2").action(e=>{try{en(e)}catch(t){q(t)}});I.command("hook").description("emit Claude Code hook JSON summarizing hotspot drift since a base ref (use in PostToolUse/Stop hooks)").option("--base <ref>","base ref to compare against (default: HEAD \u2014 i.e. working tree vs last commit)","HEAD").option("--event <name>","hook event name (selects the right output envelope: hookSpecificOutput.additionalContext for SessionStart / UserPromptSubmit / Pre|PostToolUse / etc., or top-level systemMessage for Stop / SubagentStop / ConfigChange / PreCompact)","Stop").option("--months <n>","churn window in months","3").option("--significant-percent <n>","minimum |percent change| for a stable-tier score change to be surfaced (tier transitions are always surfaced)").option("--min-degree <n>","minimum coupling degree (%) for co-change reminders. Default 50 \u2014 recall-tuned for diff-scoped queries; raise to 70+ for stricter signal",Vt).action(e=>{nn(e)});I.command("init").description("generate a starter .obsignore based on project structure").action(()=>{try{on()}catch(e){q(e)}});function V(e){return[...ye(),...e??[]]}function Ye(){return z(".obsignore")||z(".obsceneignore")}function fe(){Ye()||process.stderr.write("hint: no .obsignore found \u2014 run `obscene init` to generate one with recommended exclusions\n")}function me(){let e=Se();if(!e)return;let t;try{t=Ve(process.cwd())}catch{return}let n=(()=>{try{return Ve(e)}catch{return e}})();if(t===n)return;let o=jt(n,t)||".";process.stderr.write(`warning: scanning subtree '${o}' only \u2014 cd to repo root for whole-repo results (GH#13) | ||
| `)}function Xe(e){let t=Ie(e);return t.underCovered&&process.stderr.write(`warning: git history covers ~${t.spanDays}d, but --months window is ${t.windowDays}d \u2014 count-based confidence won't reflect time-based trust on a young repo | ||
| `),t}function qt(e){fe(),me();let t=parseInt(e.top,10),n=V(e.exclude),o=N(n),i=o.reduce((l,u)=>({totalComplexity:l.totalComplexity+u.complexity,totalCode:l.totalCode+u.code,totalLines:l.totalLines+u.lines}),{totalComplexity:0,totalCode:0,totalLines:0}),s=t>0?o.slice(0,t):o,r={generated:new Date().toISOString(),guide:Ut,summary:{...i,fileCount:o.length,avgComplexityPerFile:o.length>0?Math.round(i.totalComplexity/o.length*10)/10:0,showing:s.length},files:s};e.format==="table"?process.stdout.write(`${He(r)} | ||
| Metric concept: RRF (Cormack et al. 2009) \xB7 Strength: robust to outliers, no normalization needed \xB7 Limit: equal weight across all dimensions`,tier:"Relative ranking within THIS codebase (top 50% = hot, next 30% = warm, bottom 20% = cool). NOT an absolute quality grade \u2014 a hot file is under heavy load, not necessarily broken.",corpus:"Aggregate stats for the analyzed file set (post-exclude \u2014 files filtered by .obsignore or --exclude are not counted). When totalComplexity is 0, the rankings reflect size and churn only; HOT/WARM/COOL become relative groupings rather than risk labels.",confidence:"Epistemic stamp on each ranking \u2014 INCONCLUSIVE / WEAK / PLAUSIBLE / ACCEPTABLE. These are engineering-judgment sample-size tiers, with the weak floor for defects matching code-maat's --min-revs default of 5. ACCEPTABLE is the ceiling \u2014 the tool never claims certainty about code quality, only that the sample supports the ranking. INCONCLUSIVE rankings are surfaced under skipped rather than ranked.",reawakened:"Files that were dormant for \u2265 3\xD7 the churn window and just got touched again inside it. The objective rule: gap between the latest pre-window commit and the earliest in-window commit must be \u2265 minDormancyDays. Reawakened code often carries forgotten context \u2014 the original author may be gone, mental models stale.\nMetric concept: forensic 'reawakened files' signal from Tornhill, *Your Code as a Crime Scene* (2nd ed., Ch. 2) \xB7 Strength: highlights risk that pure churn \xD7 complexity misses \xB7 Limit: pre-window history must exist (omits truly new files), and `git log --follow` isn't used so file renames break the dormancy chain."},Gt={cochanges:"Times both files appeared in the same commit. Higher values suggest a dependency between the files. Same-directory pairs are excluded \u2014 only cross-directory pairs are shown.",degree:"Percentage: shared commits / min(churn of file1, file2) \xD7 100. Shows how tightly coupled the pair is relative to their individual change rates. 100% means every change to the less-active file also touched the other.",totalComplexity:"Sum of both files' cyclomatic complexity. Highlights coupled pairs where the involved code is also complex \u2014 hidden dependency + high complexity compounds maintenance risk.",tier:"Relative ranking within THIS codebase's coupling pairs (top 50% = hot, next 30% = warm, bottom 20% = cool). NOT an absolute quality grade. 'hot' means this pair co-changes more than most \u2014 it may be intentional and fine.",deleted:"file1Deleted / file2Deleted are set when the file is no longer present at HEAD (deleted or renamed away). The coupling signal is historical \u2014 the pair is not actionable in the current tree.",lockstep:"Set when shared commits / max(churn) \u2265 0.9 \u2014 both files almost always change together over the window. Typical of generator/mirror pairs (README \u2194 src/README, *.pb.go \u2194 *.proto). The coupling signal is real but uninformative; treat the pair as a single unit from git's perspective.",confidence:"Epistemic stamp on the coupling table \u2014 INCONCLUSIVE / WEAK / PLAUSIBLE / ACCEPTABLE. Tied to the number of commits in the analysis window. The weak floor of 5 matches code-maat's --min-revs default (Adam Tornhill); higher tiers are engineering judgment. ACCEPTABLE means the sample supports the ranking; it never asserts the couplings themselves are bad.",sumOfCoupling:'Per-file Sum of Coupling. `strength` = \u03A3 pair cochange counts a file participates in \u2014 equivalent to the SoC analysis in code-maat (\u03A3(changeset_size \u2212 1); see github.com/adamtornhill/code-maat), restricted here to cross-directory pairs and commits touching \u226420 files; near-lockstep pairs (count/max(churn) \u2265 0.9) are suppressed so mirror/generator artifacts don\'t dominate. `partners` = distinct co-change partners (graph degree). Treat as a navigation aid \u2014 high `strength` says "this file\'s couplings deserve a closer look", not "this file is buggy". EXPERIMENTAL: this surface has NOT been independently validated against defect data; it may change, be reframed, or be removed.'},Je=["json","table","compact"];function zt(e){if(!Je.includes(e))throw new Ye(`must be one of: ${Je.join(", ")}`);return e}function Vt(e){let t=Number.parseFloat(e);if(!Number.isFinite(t)||t<0||t>100)throw new Ye("must be a number between 0 and 100");return t}function fe(e){return e.option("--top <n>","limit to top N entries (0 = all)","20").option("--format <type>","output format: json | table | compact (compact = terse plain-text lines for hooks and quick reads)",zt,"json").option("--exclude <patterns...>","additional file patterns to exclude (also reads .obsignore / .obsceneignore)")}fe(I.command("report").description("per-file complexity data")).action(e=>{try{qt(e)}catch(t){X(t)}});fe(I.command("hotspots",{isDefault:!0}).description("churn \xD7 complexity hotspot analysis (default)")).option("--months <n>","churn window in months","3").option("--base [ref]","delta mode: filter rankings to files changed since this ref (bare flag auto-detects main/master)").option("--full-delta","with --base, run the full hotspots pipeline against the base ref too and emit a structured before/after diff (slower; tier transitions, score deltas, new/deleted files)").option("--paths <files...>","filter displayed entries to these paths (tiers stay corpus-anchored \u2014 answers 'are MY changes in hot territory?')").option("--since <ref>","shorthand for --paths $(git diff --name-only <ref>...HEAD) \u2014 filter to files changed since ref").option("--churn-mode <mode>","how to count churn: 'commits' counts commits touching each file; 'lines' sums added+deleted lines via git log --numstat","commits").option("--working","compare working tree (incl. uncommitted edits) against HEAD \u2014 answers 'did my refactor move the needle?' mid-work, before commit. Snapshots HEAD via a detached worktree and runs the full pipeline against both sides; produces a structured before/after diff (composite + tier transitions). Mutually exclusive with --base, --paths, --since.").action(e=>{try{Zt(e)}catch(t){X(t)}});fe(I.command("coupling").description("temporal coupling \u2014 files that change together across directories")).option("--months <n>","churn window in months","3").option("--min-cochanges <n>","minimum shared commits to include","2").action(e=>{try{en(e)}catch(t){X(t)}});I.command("hook").description("emit Claude Code hook JSON summarizing hotspot drift since a base ref (use in PostToolUse/Stop hooks)").option("--base <ref>","base ref to compare against (default: HEAD \u2014 i.e. working tree vs last commit)","HEAD").option("--event <name>","hook event name (selects the right output envelope: hookSpecificOutput.additionalContext for SessionStart / UserPromptSubmit / Pre|PostToolUse / etc., or top-level systemMessage for Stop / SubagentStop / ConfigChange / PreCompact)","Stop").option("--months <n>","churn window in months","3").option("--significant-percent <n>","minimum |percent change| for a stable-tier score change to be surfaced (tier transitions are always surfaced)").option("--min-degree <n>","minimum coupling degree (%) for co-change reminders. Default 50 \u2014 recall-tuned for diff-scoped queries; raise to 70+ for stricter signal",Vt).action(e=>{on(e)});I.command("init").description("generate a starter .obsignore based on project structure").action(()=>{try{rn()}catch(e){X(e)}});function Y(e){return[...ye(),...e??[]]}function Xe(){return K(".obsignore")||K(".obsceneignore")}function de(){Xe()||process.stderr.write("hint: no .obsignore found \u2014 run `obscene init` to generate one with recommended exclusions\n")}function me(){let e=$e();if(!e)return;let t;try{t=qe(process.cwd())}catch{return}let n=(()=>{try{return qe(e)}catch{return e}})();if(t===n)return;let o=jt(n,t)||".";process.stderr.write(`warning: scanning subtree '${o}' only \u2014 cd to repo root for whole-repo results (GH#13) | ||
| `)}function Qe(e){let t=Fe(e);return t.underCovered&&process.stderr.write(`warning: git history covers ~${t.spanDays}d, but --months window is ${t.windowDays}d \u2014 count-based confidence won't reflect time-based trust on a young repo | ||
| `),t}function qt(e){de(),me();let t=parseInt(e.top,10),n=Y(e.exclude),o=N(n),i=o.reduce((l,u)=>({totalComplexity:l.totalComplexity+u.complexity,totalCode:l.totalCode+u.code,totalLines:l.totalLines+u.lines}),{totalComplexity:0,totalCode:0,totalLines:0}),s=t>0?o.slice(0,t):o,r={generated:new Date().toISOString(),guide:Ut,summary:{...i,fileCount:o.length,avgComplexityPerFile:o.length>0?Math.round(i.totalComplexity/o.length*10)/10:0,showing:s.length},files:s};e.format==="table"?process.stdout.write(`${Ne(r)} | ||
| `):e.format==="compact"?process.stdout.write(`${Pe(r)} | ||
| `):process.stdout.write(`${JSON.stringify(r,null,2)} | ||
| `)}function Jt(e,t,n){for(let o of Object.values(e))for(let i of o.entries){let s=n.get(i.file);s&&(i.complexityDelta=s)}if(t)for(let o of t.entries){let i=n.get(o.file);i&&(o.complexityDelta=i)}}function Kt(e){if(typeof e=="string")return e;let t=ke();if(!t)throw new Error("--base used without a ref but no default branch found (looked for main, master). Specify the base ref explicitly, e.g. --base <branch-or-sha>.");return t}function Yt(e){if(e.paths&&e.paths.length>0&&e.since)throw new Error("--paths and --since are mutually exclusive \u2014 pick one path-source.");return e.paths&&e.paths.length>0?{paths:new Set(e.paths),source:`--paths (${e.paths.length} file${e.paths.length===1?"":"s"})`}:e.since?{paths:ie(e.since),source:`--since ${e.since}`}:null}function Xt(e,t){let n=e.composite?.tierCounts.hot??0,o=e.composite?.totalEntries??0,i=new Set;for(let a of Object.values(e.rankings))for(let f of a.entries)i.add(f.file);for(let a of e.composite?.entries??[])i.add(a.file);for(let a of Object.values(e.rankings))a.entries=a.entries.filter(f=>t.paths.has(f.file)),a.showing=a.entries.length;e.composite&&(e.composite.entries=e.composite.entries.filter(a=>t.paths.has(a.file)),e.composite.showing=e.composite.entries.length),e.reawakened&&(e.reawakened.entries=e.reawakened.entries.filter(a=>t.paths.has(a.file)),e.reawakened.entries.length===0&&(e.reawakened=void 0));let s=0,r=0,l=0;for(let a of e.composite?.entries??[])a.tier==="hot"?s++:a.tier==="warm"?r++:l++;let u=[...t.paths].filter(a=>!i.has(a)).sort(),c=o>0?`${Math.round(n/o*100)}%`:"n/a",p=s+r+l;process.stderr.write(`path filter ${t.source}: ${s} HOT, ${r} WARM, ${l} COOL of ${p} ranked file${p===1?"":"s"}; ${u.length} not in any ranking. Corpus base rate: ${c} HOT. | ||
| `),e.pathFilter={source:t.source,paths:[...t.paths].sort(),rankedCount:p,hotCount:s,warmCount:r,coolCount:l,notRanked:u,corpusHotRate:o>0?n/o:null}}function Qt(e){if(e==="commits"||e==="lines")return e;throw new Error(`Unknown --churn-mode '${e}'. Expected 'commits' or 'lines'.`)}function Zt(e){fe(),me();let t=Ye(),n=parseInt(e.top,10),o=parseInt(e.months,10),i=Qt(e.churnMode),s=Xe(o),r=V(e.exclude),l=N(r);if(e.fullDelta&&e.base===void 0)throw new Error("--full-delta requires --base. Specify a base ref, e.g. --base main --full-delta.");let u=Yt(e);if(u&&e.base!==void 0)throw new Error('--paths/--since and --base are mutually exclusive \u2014 they answer different questions. --paths gives corpus-anchored tiers ("are MY changes hot in the codebase?"); --base re-ranks within the changed set ("of my changes, which is hottest?").');let c,p,a;if(e.base!==void 0){let x=Kt(e.base),h=ie(x);if(h.size===0){process.stderr.write(`No files changed since ${x}. | ||
| `);let b={generated:new Date().toISOString(),guide:qe,churnWindow:`${o} months`,churnMode:i,historyCoverage:s,delta:{base:x,head:"HEAD",changedFiles:[]},rankings:{},corpus:{fileCount:0,totalComplexity:0,filtered:t}};e.format==="table"?process.stdout.write(`${le(b)} | ||
| `):e.format==="compact"?process.stdout.write(`${ue(b)} | ||
| `):process.stdout.write(`${JSON.stringify(b,null,2)} | ||
| `);return}if(c={base:x,head:"HEAD",changedFiles:[...h].sort()},e.fullDelta)try{let b=B(x,v=>se({months:o,excludes:r,cwd:v,churnMode:i}));a=L(l,o,0,void 0,i);let S={files:l,rankings:a.rankings,skipped:a.skipped,composite:a.composite,reawakened:a.reawakened,corpus:a.corpus};p=ae(x,"HEAD",b,S)}catch(b){let S=b instanceof Error?b.message:String(b);process.stderr.write(`warning: full-delta unavailable (${S}). Falling back to filtered rankings. | ||
| `),l=l.filter(v=>h.has(v.file)),a=void 0,c.fallback={from:"full-delta",reason:S}}else l=l.filter(b=>h.has(b.file))}let{rankings:f,skipped:g,composite:w,corpus:k,reawakened:d}=a?Me(a,n):L(l,o,n,void 0,i);if(c&&p===void 0){let x=new Map,h=[];for(let b of l)x.set(b.file,b.complexity),h.push(b.file);try{let b=$e(c.base,h,x);Jt(f,w,b)}catch(b){let S=b instanceof Error?b.message:String(b);process.stderr.write(`warning: complexity delta unavailable (${S}). Falling back to filtered rankings without per-file deltas. | ||
| `)}}let m={generated:new Date().toISOString(),guide:qe,churnWindow:`${o} months`,churnMode:i,historyCoverage:s,delta:c,fullDelta:p,rankings:f,skipped:Object.keys(g).length>0?g:void 0,composite:w,reawakened:d.entries.length>0?d:void 0,corpus:{...k,filtered:t}};u&&Xt(m,u),e.format==="compact"?process.stdout.write(`${ue(m)} | ||
| `):e.format==="table"?(process.stdout.write(`${le(m)} | ||
| `),w.entries.length>0&&process.stdout.write(` | ||
| ${je(w)} | ||
| `)):process.stdout.write(`${JSON.stringify(m,null,2)} | ||
| `)}function en(e){fe(),me();let t=parseInt(e.top,10),n=parseInt(e.months,10),o=parseInt(e.minCochanges,10),i=Xe(n),s=V(e.exclude),r=N(s),l=W(n),u=ee(n,s),c=new Map;for(let x of r)c.set(x.file,x.complexity);let p=re(),a=ve(u,l,c,o,p),f=t>0?a.slice(0,t):a,g={hot:0,warm:0,cool:0};for(let x of a)g[x.tier]++;let w=a.reduce((x,h)=>x+h.couplingScore,0),k=Re(u,o,l,p),d=t>0?k.slice(0,t):k,m={generated:new Date().toISOString(),guide:Gt,churnWindow:`${n} months`,historyCoverage:i,minCochanges:o,totalScore:w,tierCounts:g,tiers:g,totalCouplings:a.length,showing:f.length,couplings:f,sumOfCoupling:d.length>0?d:void 0,confidence:Oe(Te(n))};e.format==="table"?process.stdout.write(`${Ae(m)} | ||
| `):e.format==="compact"?process.stdout.write(`${We(m)} | ||
| `):process.stdout.write(`${JSON.stringify(m,null,2)} | ||
| `)}function tn(){try{if(ze("git",["diff","--quiet","HEAD"],{stdio:["ignore","ignore","ignore"]}).status!==0)return!1;let t=ze("git",["ls-files","--others","--exclude-standard"],{encoding:"utf-8"});return t.status!==0?!1:t.stdout.trim().length===0}catch{return!1}}function nn(e){try{if(e.base==="HEAD"&&tn())return;let t=parseInt(e.months,10),n=V(),o=N(n),i=B(e.base,f=>se({months:t,excludes:n,cwd:f})),s=L(o,t,0),r={files:o,rankings:s.rankings,skipped:s.skipped,composite:s.composite,reawakened:s.reawakened,corpus:s.corpus},l=ae(e.base,"HEAD",i,r),u=e.significantPercent!==void 0?parseFloat(e.significantPercent):void 0,c=new Set;try{let f=Pt(`git diff --name-only ${e.base}`,{maxBuffer:52428800,stdio:["pipe","pipe","pipe"]}).toString().split(` | ||
| `);for(let g of f){let w=g.trim();w&&c.add(w)}}catch{}let p=[];if(c.size>0){let f=W(t),g=ee(t,n);p=xe(g,f,c,{minDegree:e.minDegree})}let a=Ue(l,{significantPercentChange:u!==void 0&&!Number.isNaN(u)?u:void 0,reminders:p,reminderMinDegree:e.minDegree});if(a===null)return;process.stdout.write(JSON.stringify(Ge(a,e.event)))}catch{}}function on(){if(z(".obsignore"))throw new Error(".obsignore already exists. Remove it first to regenerate.");if(z(".obsceneignore"))throw new Error(".obsceneignore already exists. Remove it first to regenerate.");let e=De(),t=Ee(e);Wt(".obsignore",t);let n=Z.reduce((o,i)=>o+i.patterns.length,0);if(process.stderr.write(`Created .obsignore with ${n} universal exclusions`),e.length>0){process.stderr.write(` + ${e.length} detected patterns: | ||
| `)}function Jt(e,t,n){for(let o of Object.values(e))for(let i of o.entries){let s=n.get(i.file);s&&(i.complexityDelta=s)}if(t)for(let o of t.entries){let i=n.get(o.file);i&&(o.complexityDelta=i)}}function Kt(e){if(typeof e=="string")return e;let t=Se();if(!t)throw new Error("--base used without a ref but no default branch found (looked for main, master). Specify the base ref explicitly, e.g. --base <branch-or-sha>.");return t}function Yt(e){if(e.paths&&e.paths.length>0&&e.since)throw new Error("--paths and --since are mutually exclusive \u2014 pick one path-source.");return e.paths&&e.paths.length>0?{paths:new Set(e.paths),source:`--paths (${e.paths.length} file${e.paths.length===1?"":"s"})`}:e.since?{paths:ce(e.since),source:`--since ${e.since}`}:null}function Xt(e,t){let n=e.composite?.tierCounts.hot??0,o=e.composite?.totalEntries??0,i=new Set;for(let a of Object.values(e.rankings))for(let f of a.entries)i.add(f.file);for(let a of e.composite?.entries??[])i.add(a.file);for(let a of Object.values(e.rankings))a.entries=a.entries.filter(f=>t.paths.has(f.file)),a.showing=a.entries.length;e.composite&&(e.composite.entries=e.composite.entries.filter(a=>t.paths.has(a.file)),e.composite.showing=e.composite.entries.length),e.reawakened&&(e.reawakened.entries=e.reawakened.entries.filter(a=>t.paths.has(a.file)),e.reawakened.entries.length===0&&(e.reawakened=void 0));let s=0,r=0,l=0;for(let a of e.composite?.entries??[])a.tier==="hot"?s++:a.tier==="warm"?r++:l++;let u=[...t.paths].filter(a=>!i.has(a)).sort(),c=o>0?`${Math.round(n/o*100)}%`:"n/a",p=s+r+l;process.stderr.write(`path filter ${t.source}: ${s} HOT, ${r} WARM, ${l} COOL of ${p} ranked file${p===1?"":"s"}; ${u.length} not in any ranking. Corpus base rate: ${c} HOT. | ||
| `),e.pathFilter={source:t.source,paths:[...t.paths].sort(),rankedCount:p,hotCount:s,warmCount:r,coolCount:l,notRanked:u,corpusHotRate:o>0?n/o:null}}function Qt(e){if(e==="commits"||e==="lines")return e;throw new Error(`Unknown --churn-mode '${e}'. Expected 'commits' or 'lines'.`)}function Zt(e){de(),me();let t=Xe(),n=parseInt(e.top,10),o=parseInt(e.months,10),i=Qt(e.churnMode),s=Qe(o),r=Y(e.exclude),l=N(r);if(e.fullDelta&&e.base===void 0)throw new Error("--full-delta requires --base. Specify a base ref, e.g. --base main --full-delta.");if(e.working&&e.base!==void 0)throw new Error("--working and --base are mutually exclusive. --working compares working tree vs HEAD; --base compares against a committed ref.");let u=Yt(e);if(u&&e.base!==void 0)throw new Error('--paths/--since and --base are mutually exclusive \u2014 they answer different questions. --paths gives corpus-anchored tiers ("are MY changes hot in the codebase?"); --base re-ranks within the changed set ("of my changes, which is hottest?").');if(u&&e.working)throw new Error("--paths/--since and --working are mutually exclusive \u2014 --working already scopes to files in the working tree diff.");let c,p,a;if(e.working){let C=nn();if(C.size===0){process.stderr.write(`No working-tree changes vs HEAD \u2014 nothing to delta. | ||
| `);let h={generated:new Date().toISOString(),guide:pe,churnWindow:`${o} months`,churnMode:i,historyCoverage:s,delta:{base:"HEAD",head:"WORKING",changedFiles:[]},rankings:{},corpus:{fileCount:0,totalComplexity:0,filtered:t}};e.format==="table"?process.stdout.write(`${q(h)} | ||
| `):e.format==="compact"?process.stdout.write(`${J(h)} | ||
| `):process.stdout.write(`${JSON.stringify(h,null,2)} | ||
| `);return}c={base:"HEAD",head:"WORKING",changedFiles:[...C].sort()};try{let h=_("HEAD",S=>U({months:o,excludes:r,cwd:S,churnMode:i}));a=A(l,o,0,void 0,i);let y={files:l,rankings:a.rankings,skipped:a.skipped,composite:a.composite,reawakened:a.reawakened,corpus:a.corpus};p=G("HEAD","WORKING",h,y)}catch(h){let y=h instanceof Error?h.message:String(h);process.stderr.write(`warning: --working snapshot unavailable (${y}). Falling back to filtered rankings against the working tree only. | ||
| `),l=l.filter(S=>C.has(S.file)),a=void 0,c.fallback={from:"working",reason:y}}}else if(e.base!==void 0){let C=Kt(e.base),h=ce(C);if(h.size===0){process.stderr.write(`No files changed since ${C}. | ||
| `);let y={generated:new Date().toISOString(),guide:pe,churnWindow:`${o} months`,churnMode:i,historyCoverage:s,delta:{base:C,head:"HEAD",changedFiles:[]},rankings:{},corpus:{fileCount:0,totalComplexity:0,filtered:t}};e.format==="table"?process.stdout.write(`${q(y)} | ||
| `):e.format==="compact"?process.stdout.write(`${J(y)} | ||
| `):process.stdout.write(`${JSON.stringify(y,null,2)} | ||
| `);return}if(c={base:C,head:"HEAD",changedFiles:[...h].sort()},e.fullDelta)try{let y=_(C,v=>U({months:o,excludes:r,cwd:v,churnMode:i}));a=A(l,o,0,void 0,i);let S={files:l,rankings:a.rankings,skipped:a.skipped,composite:a.composite,reawakened:a.reawakened,corpus:a.corpus};p=G(C,"HEAD",y,S)}catch(y){let S=y instanceof Error?y.message:String(y);process.stderr.write(`warning: full-delta unavailable (${S}). Falling back to filtered rankings. | ||
| `),l=l.filter(v=>h.has(v.file)),a=void 0,c.fallback={from:"full-delta",reason:S}}else l=l.filter(y=>h.has(y.file))}let{rankings:f,skipped:g,composite:b,corpus:k,reawakened:m}=a?Me(a,n):A(l,o,n,void 0,i);if(c&&p===void 0){let C=new Map,h=[];for(let y of l)C.set(y.file,y.complexity),h.push(y.file);try{let y=ve(c.base,h,C);Jt(f,b,y)}catch(y){let S=y instanceof Error?y.message:String(y);process.stderr.write(`warning: complexity delta unavailable (${S}). Falling back to filtered rankings without per-file deltas. | ||
| `)}}let d={generated:new Date().toISOString(),guide:pe,churnWindow:`${o} months`,churnMode:i,historyCoverage:s,delta:c,fullDelta:p,rankings:f,skipped:Object.keys(g).length>0?g:void 0,composite:b,reawakened:m.entries.length>0?m:void 0,corpus:{...k,filtered:t}};u&&Xt(d,u),e.format==="compact"?process.stdout.write(`${J(d)} | ||
| `):e.format==="table"?(process.stdout.write(`${q(d)} | ||
| `),b.entries.length>0&&process.stdout.write(` | ||
| ${Be(b)} | ||
| `)):process.stdout.write(`${JSON.stringify(d,null,2)} | ||
| `)}function en(e){de(),me();let t=parseInt(e.top,10),n=parseInt(e.months,10),o=parseInt(e.minCochanges,10),i=Qe(n),s=Y(e.exclude),r=N(s),l=j(n),u=ie(n,s),c=new Map;for(let C of r)c.set(C.file,C.complexity);let p=le(),a=Ee(u,l,c,o,p),f=t>0?a.slice(0,t):a,g={hot:0,warm:0,cool:0};for(let C of a)g[C.tier]++;let b=a.reduce((C,h)=>C+h.couplingScore,0),k=De(u,o,l,p),m=t>0?k.slice(0,t):k,d={generated:new Date().toISOString(),guide:Gt,churnWindow:`${n} months`,historyCoverage:i,minCochanges:o,totalScore:b,tierCounts:g,tiers:g,totalCouplings:a.length,showing:f.length,couplings:f,sumOfCoupling:m.length>0?m:void 0,confidence:Te(Ie(n))};e.format==="table"?process.stdout.write(`${Le(d)} | ||
| `):e.format==="compact"?process.stdout.write(`${je(d)} | ||
| `):process.stdout.write(`${JSON.stringify(d,null,2)} | ||
| `)}function tn(){try{if(Ve("git",["diff","--quiet","HEAD"],{stdio:["ignore","ignore","ignore"]}).status!==0)return!1;let t=Ve("git",["ls-files","--others","--exclude-standard"],{encoding:"utf-8"});return t.status!==0?!1:t.stdout.trim().length===0}catch{return!1}}function nn(){let e=new Set,t=Ke("git diff --name-only HEAD",{maxBuffer:50*1024*1024,stdio:["pipe","pipe","pipe"]}).toString().split(` | ||
| `);for(let n of t){let o=n.trim();o&&e.add(o)}return e}function on(e){try{if(e.base==="HEAD"&&tn())return;let t=parseInt(e.months,10),n=Y(),o=N(n),i=_(e.base,f=>U({months:t,excludes:n,cwd:f})),s=A(o,t,0),r={files:o,rankings:s.rankings,skipped:s.skipped,composite:s.composite,reawakened:s.reawakened,corpus:s.corpus},l=G(e.base,"HEAD",i,r),u=e.significantPercent!==void 0?parseFloat(e.significantPercent):void 0,c=new Set;try{let f=Ke(`git diff --name-only ${e.base}`,{maxBuffer:52428800,stdio:["pipe","pipe","pipe"]}).toString().split(` | ||
| `);for(let g of f){let b=g.trim();b&&c.add(b)}}catch{}let p=[];if(c.size>0){let f=j(t),g=ie(t,n);p=ke(g,f,c,{minDegree:e.minDegree})}let a=Ge(l,{significantPercentChange:u!==void 0&&!Number.isNaN(u)?u:void 0,reminders:p,reminderMinDegree:e.minDegree});if(a===null)return;process.stdout.write(JSON.stringify(ze(a,e.event)))}catch{}}function rn(){if(K(".obsignore"))throw new Error(".obsignore already exists. Remove it first to regenerate.");if(K(".obsceneignore"))throw new Error(".obsceneignore already exists. Remove it first to regenerate.");let e=Re(),t=Oe(e);Pt(".obsignore",t);let n=oe.reduce((o,i)=>o+i.patterns.length,0);if(process.stderr.write(`Created .obsignore with ${n} universal exclusions`),e.length>0){process.stderr.write(` + ${e.length} detected patterns: | ||
| `);for(let o of e)process.stderr.write(` ${o.pattern.padEnd(20)} ${o.comment} | ||
| `)}else process.stderr.write(` (no project-specific patterns detected) | ||
| `)}function q(e){let t=e instanceof Error?e.message:String(e);process.stderr.write(`Error: ${t} | ||
| `)}function X(e){let t=e instanceof Error?e.message:String(e);process.stderr.write(`Error: ${t} | ||
| `),process.exit(1)}I.parse(); |
+1
-1
| { | ||
| "name": "@wbern/obscene", | ||
| "version": "2.13.0", | ||
| "version": "2.14.0", | ||
| "description": "Identify hotspot files — complex code that changes frequently. Churn × complexity analysis for any git repo.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
132582
1.5%256
4.49%