From a31093822ced797451e2fff8365c8e77c478d2ac Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Mar 2026 21:09:13 +0000 Subject: [PATCH] Add event-taxonomy package with canonical schema, adapters, and CLI Canonical NormalizedFinding schema with Severity enum (CRITICAL/HIGH/MEDIUM/LOW/INFO). Per-project adapters for 9 tools with severity mapping for string labels, int 1-10, float 0-1, Python Enum, and computed properties. CLI pipe interface and registry. Nightshift-Task: event-taxonomy Nightshift-Ref: https://github.com/marcus/nightshift --- pyproject.toml | 19 +++ src/event_taxonomy/__init__.py | 3 + src/event_taxonomy/__main__.py | 32 ++++ .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 282 bytes .../__pycache__/schema.cpython-313.pyc | Bin 0 -> 3922 bytes src/event_taxonomy/adapters/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 161 bytes .../__pycache__/_severity.cpython-313.pyc | Bin 0 -> 3563 bytes .../__pycache__/bus_factor.cpython-313.pyc | Bin 0 -> 1034 bytes .../__pycache__/dep_risk.cpython-313.pyc | Bin 0 -> 1367 bytes .../__pycache__/doc_drift.cpython-313.pyc | Bin 0 -> 985 bytes .../knowledge_silo.cpython-313.pyc | Bin 0 -> 1352 bytes .../perf_regression.cpython-313.pyc | Bin 0 -> 999 bytes .../roadmap_entropy.cpython-313.pyc | Bin 0 -> 1052 bytes .../schema_evolution.cpython-313.pyc | Bin 0 -> 1168 bytes .../__pycache__/semantic_diff.cpython-313.pyc | Bin 0 -> 1441 bytes ...est_flakiness.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 1295 bytes src/event_taxonomy/adapters/_severity.py | 85 ++++++++++ src/event_taxonomy/adapters/bus_factor.py | 20 +++ src/event_taxonomy/adapters/dep_risk.py | 22 +++ src/event_taxonomy/adapters/doc_drift.py | 16 ++ src/event_taxonomy/adapters/knowledge_silo.py | 24 +++ .../adapters/perf_regression.py | 16 ++ .../adapters/roadmap_entropy.py | 26 ++++ .../adapters/schema_evolution.py | 22 +++ src/event_taxonomy/adapters/semantic_diff.py | 28 ++++ src/event_taxonomy/adapters/test_flakiness.py | 23 +++ src/event_taxonomy/registry.py | 48 ++++++ src/event_taxonomy/schema.py | 68 ++++++++ ...test_adapters.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 20387 bytes .../test_schema.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 13667 bytes ...test_severity.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 31199 bytes tests/test_adapters.py | 145 ++++++++++++++++++ tests/test_schema.py | 52 +++++++ tests/test_severity.py | 60 ++++++++ 35 files changed, 709 insertions(+) create mode 100644 pyproject.toml create mode 100644 src/event_taxonomy/__init__.py create mode 100644 src/event_taxonomy/__main__.py create mode 100644 src/event_taxonomy/__pycache__/__init__.cpython-313.pyc create mode 100644 src/event_taxonomy/__pycache__/schema.cpython-313.pyc create mode 100644 src/event_taxonomy/adapters/__init__.py create mode 100644 src/event_taxonomy/adapters/__pycache__/__init__.cpython-313.pyc create mode 100644 src/event_taxonomy/adapters/__pycache__/_severity.cpython-313.pyc create mode 100644 src/event_taxonomy/adapters/__pycache__/bus_factor.cpython-313.pyc create mode 100644 src/event_taxonomy/adapters/__pycache__/dep_risk.cpython-313.pyc create mode 100644 src/event_taxonomy/adapters/__pycache__/doc_drift.cpython-313.pyc create mode 100644 src/event_taxonomy/adapters/__pycache__/knowledge_silo.cpython-313.pyc create mode 100644 src/event_taxonomy/adapters/__pycache__/perf_regression.cpython-313.pyc create mode 100644 src/event_taxonomy/adapters/__pycache__/roadmap_entropy.cpython-313.pyc create mode 100644 src/event_taxonomy/adapters/__pycache__/schema_evolution.cpython-313.pyc create mode 100644 src/event_taxonomy/adapters/__pycache__/semantic_diff.cpython-313.pyc create mode 100644 src/event_taxonomy/adapters/__pycache__/test_flakiness.cpython-313-pytest-9.0.2.pyc create mode 100644 src/event_taxonomy/adapters/_severity.py create mode 100644 src/event_taxonomy/adapters/bus_factor.py create mode 100644 src/event_taxonomy/adapters/dep_risk.py create mode 100644 src/event_taxonomy/adapters/doc_drift.py create mode 100644 src/event_taxonomy/adapters/knowledge_silo.py create mode 100644 src/event_taxonomy/adapters/perf_regression.py create mode 100644 src/event_taxonomy/adapters/roadmap_entropy.py create mode 100644 src/event_taxonomy/adapters/schema_evolution.py create mode 100644 src/event_taxonomy/adapters/semantic_diff.py create mode 100644 src/event_taxonomy/adapters/test_flakiness.py create mode 100644 src/event_taxonomy/registry.py create mode 100644 src/event_taxonomy/schema.py create mode 100644 tests/__pycache__/test_adapters.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_schema.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_severity.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/test_adapters.py create mode 100644 tests/test_schema.py create mode 100644 tests/test_severity.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0c64f4e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "event-taxonomy" +version = "0.1.0" +description = "Normalize event naming and structure across analysis tools" +requires-python = ">=3.13" +dependencies = [] + +[project.scripts] +event-taxonomy = "event_taxonomy.__main__:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/event_taxonomy"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/src/event_taxonomy/__init__.py b/src/event_taxonomy/__init__.py new file mode 100644 index 0000000..465b0f4 --- /dev/null +++ b/src/event_taxonomy/__init__.py @@ -0,0 +1,3 @@ +from event_taxonomy.schema import NormalizedFinding, Severity, ToolEvent + +__all__ = ["NormalizedFinding", "Severity", "ToolEvent"] diff --git a/src/event_taxonomy/__main__.py b/src/event_taxonomy/__main__.py new file mode 100644 index 0000000..a29890f --- /dev/null +++ b/src/event_taxonomy/__main__.py @@ -0,0 +1,32 @@ +"""CLI: reads JSON from stdin, outputs normalized JSON. + +Usage: + echo '[{...}]' | python -m event_taxonomy --tool bus-factor-analyzer + echo '[{...}]' | event-taxonomy --tool dep-risk-scanner +""" + +import argparse +import json +import sys + +from event_taxonomy.registry import list_tools, normalize_output + + +def main() -> None: + parser = argparse.ArgumentParser(description="Normalize tool findings to canonical schema") + parser.add_argument("--tool", required=True, help=f"Tool name. Options: {', '.join(list_tools())}") + parser.add_argument("--version", default="unknown", help="Tool version string") + parser.add_argument("--indent", type=int, default=2, help="JSON indent (0 for compact)") + args = parser.parse_args() + + raw = json.load(sys.stdin) + if isinstance(raw, dict): + raw = [raw] + + event = normalize_output(args.tool, raw, tool_version=args.version) + indent = args.indent if args.indent > 0 else None + print(event.to_json(indent=indent)) + + +if __name__ == "__main__": + main() diff --git a/src/event_taxonomy/__pycache__/__init__.cpython-313.pyc b/src/event_taxonomy/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2dadf721747f63d653f14d68dccd1e8ce55e48a0 GIT binary patch literal 282 zcmey&%ge<81gp)~XEp=r#~=<2FhLog6@ZMX48aV+jNS}hj75xIOhrsy%tg!!4EjK^ zK*l1LbS6#ammuYu%(n#n@{4j4b26(^Q`|E1QZn<>Z*c^tmZcVDmQ>#249U;WaV<;D zEAi80y(J11&MS#8Nvz1v%g?RUD^AWx%}p#~1{zev0wP#}#4Yyt_{5x?`1q9!pMhM4 zTju&1`Nbtg`S~UKVEwu<{rbg4$q*s9k^1rRnR%Hd@$q^EmA5!-a`RJ4b5iY!xPj&{ e0&%ebkodsN$jEq~!Q&!>$0II_M)o33pdbLKa895A literal 0 HcmV?d00001 diff --git a/src/event_taxonomy/__pycache__/schema.cpython-313.pyc b/src/event_taxonomy/__pycache__/schema.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..50db32ece4e1f31040dd9e2a97f35cc3561e2deb GIT binary patch literal 3922 zcma)9U2Gf25#Bo<$>YB$MYibQvnm& zNoIg9z}eZEz5SV)Z)Uj~i-ifaUmyOY_}>5_U*MqG{4OJ`{tlQML?x>hT zh*adgkNTkZoslN|)ISlRfr%gu@;TBOc|xZ0M2Lp?*ncKG5up)<^pIYn2DT72I4e%| zG&g+O5Bpr6#%2S2t_*WS!=fu4_9oJ}rwxNP@{BeyC#IvzI5#ag5zRC$TeFLn$ueo5 z6VO<`n6sTwUbD4aNn_0M7m9i*pB5b%WV&56bVo+IV3|6s;2SY3Id5m)d8ZLp55w>V znI%+E3D`scdx%t0eblc?G@$xvPz_L74bqSrQQ;H3P3NF}+k^_^ILyAAAdTrR0;!TlqU*iNwaSv* zvx=v=OETb*0M+d>H60&(k2cF@@l>!QBaM-DCUi1 zu8ZSnHq=XI!)oFcfkxC0n3Lplo>Mz{oJQ3y8k-BHyPaglqJ~x~F6jBwMKfPC=a$h* z3$IL@MF0*&r7KmaK2LQ9QNh8cXgO*zrf8*-qM2H$!io&S)+(_9x(&W}Bpf($4j?yY z(F(VxBR6B;2^uM1GSkvclVOiH1ptnh z17K3CO?h5y34oavQ~~WeE%cGJn4$f^r+pM>a-Kei0+Dt@=LmU*%PI;FFehkRS)Qs= z&v;Wj{FWJJCP_zb^&n!No$fPAmAXX29bON3Dga6BOZ}rW~MJA!P{n;ZzI#r%cH3*1WD< zrrP{GU`x?BNcJ+tY{5>k%JzKO_HYwI-0(E$L^%spgh9-&yF7%j8^#5G`5B;YYZth^ zWTq9mQ|QzUtr_@Jp$!TcHZR^sDCY7s)I8`MpJ`pDGUv*6&IuKnRlxh$8$!d8kCrs! z&AfKp>1^3%pee&pV;Hej^4phxfArI%f0q9e{Zq6)G|ml285UiH2Yz`woyQ`SiJ(h ze`nV()4#d67+BhMypim_x&5cx7e^Y=&YMTBAGvwr`iXjU_s`#{Mfd$Jy8DrjbnblE zvH8QU56<7dbn8+*^}NViffwn51leyk4GV~nGU0Y^&)?Vv0ue2P&dANF&JWOrBz2nx7#x~{tw*K0- z{)RUmc<~eCW24@4^kd^r@^Z7f9jatu9NZdECT)*-t66gr8kU%K>)t53!9pnDF zml{ch*ZBv%27xE6VqI7ZzIKr4#YZcCv7t5XdPo?AT6~ zQ>Y-+u2A%|TZJzo*RB*FM+FkBKdn;y(!)gO)l8$ayDoJ<=-yI$;q_Yg8`beeR+rv* zu&@6Q`P#nG>ey0zPaUeZKej4L{+CvLBoJRg?r}8e4?pP?{D<7zDDMA)<5Yv806#VhWW^^4!e^xS3ljM|iF{55Yh>_qviCEx>2V+|q!#V>u3WqF nm;k%7g@}|I2|!8P literal 0 HcmV?d00001 diff --git a/src/event_taxonomy/adapters/__init__.py b/src/event_taxonomy/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/event_taxonomy/adapters/__pycache__/__init__.cpython-313.pyc b/src/event_taxonomy/adapters/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e531d77d5346ea9755b116773d36f90a398f66a7 GIT binary patch literal 161 zcmey&%ge<81gp)~XM*U*AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekkl;){fzwLlA`?l z68+S&)VvbilEjMqy!_ls{o ZPO4oIE6_lYg~cGoM`lJw#v*1Q3jp!NDHQ+! literal 0 HcmV?d00001 diff --git a/src/event_taxonomy/adapters/__pycache__/_severity.cpython-313.pyc b/src/event_taxonomy/adapters/__pycache__/_severity.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a506b28e8ffaefc3434f9c8933a4fd7a5f3c859c GIT binary patch literal 3563 zcmd5;OHUhD6uush!FUWd5FQDk8D1(*WDgY5KoUYJA%%iNgoD~DAZuy{VYQ%z6s#Kvy;tzD!KVX*yXp}{ywhOv)M7r-e*N=d4t3ISESMuDM<1_bs z_dDl&cMhwoZ3M>m@9u*p-!C;0fcVfI<(Y`zMx^1jM5 zD)QILez(R1D4685yvddyv zM`tE(PmGRC8T|Of2jfPCckKGatk*D4O@Cx?)02kHKQ;3H*p%Npa?7xUA}pN6!%e#RTE zgM}R!p%pIcr=)wOkDiw@i6`{ zZ9RL$@F@N$ZGG#A;X(XC+S>A5s80;VhthmA%PO=*)q)&>-Ki^@VGYPDpl)!-Yx1IE z2!7w__?UMjuUF8z;bZy7?XmnX!p;AtQ|B=!h)*iOm5N(9*(G=?ur8rz@DAY?QqT?e zM|;G9J)#Aclx3MI4LuJIm(Z!uB8`dR_;Bjt)|HJbn?ujKcih|VU11>259G+?fk8M& zLQoh6zZ42C8P;WWL7}p)7-r~E!|orud3$F1)+fm58$hKSsVqR25V2GMECJq=Y)VKU zYEB886k7fmmrQT)h*hJjIWl{}V+_5{!371oX-{a$2ra3xt;vnaP4BaTouTcaU12cI z59SnyN8*mL^ri~VS0_a8-wKWkGqM<#?}j3ZrisXa*xlplarb(9T<|4Q6d_L#XoT@% zIM~O=ZsF#~;rw5aCDac%4c>w2Xr%6}f&fF&lzYq%Oh8eyn42Rz@ zMdj2y5`^0KyssSf%^Suaw*8%PLD3 qw{DLuB>Pg<gW literal 0 HcmV?d00001 diff --git a/src/event_taxonomy/adapters/__pycache__/bus_factor.cpython-313.pyc b/src/event_taxonomy/adapters/__pycache__/bus_factor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1fc860d87a1c0a91769301598aff749155151849 GIT binary patch literal 1034 zcmZ8fOKTHR6uy(0%;eps)hfldQ?b^RHWB)eJ`k)Aq=i}>+AdNYrpY87otYc&+-Xe~ z(v{#!1q)e8mWm5qy7VVZ=q6lR5XFVNlpwhD-aH!UfqTz)ULWTiP9qu(0V<#8z8L>} z0Q_P`M}#(L+@s(SC_v#>!3u{snPK0mZ-qyE6Z5OWiXZtY7PiH0X`7qz`Du;f-}cW4 ziZ}?K3Mi155`jOAGQko4j$NcGW@!Z#8O}4+)OK`}dg5ykS(<5-^y~w}&Kh=}NVzr; zk&aztXE;XEmI&26eT@(FI|S*on8GQ(G>bH411LP}Xto_!1nTiKPYfs`#ZnswC=NDz z5A^^gJS}a2$ILt@N~FWosqNHJ9c2YnH~E!VQc!y4#cLqZyH@Joah<80mcbBBX|`q- zOFAlrx=s@6Zle@lb{)BuEEATbwhWi?%Rdj^Q!j=ERdew!L?uy%`?e#KSl6c(GPuMi zZ{`R$MuePU>ZI3pEj0sejEo(Zw&5fM6sCNLZ4&jck6lDUBm#!eB!P^E^*lsH5^(fA z9T~VtBujT3Ew7^(%_IR!$68jyT7pNcGsNzO@Od34I3!Z+u%*jnIkgKNj3C4*N@U|H zti6CXw2CPQWm+!v%tZ+cSVvB(J9e^ABtg4#9_SJ!l^FXg`W=Ad@ReHhYArr=93QDj zA0t(H_9#AA>l-TPD`s_g+8dhrdjH$__sQzQBX54)Ti^7yo_H#(ZWX-e*xPhHchBqH zuSGAsNxx2)2aclhkHOK(WOXoA8y@qn&sB$S)#P#S#zIwIthFPjL4RN9G#ZJBbrA83 z4cZAbKDdHQBD63HsG}j^j?iz;Cd00;cO?loQ|L^4&^*`Qf{lbwEvT KV(~B2O|Jjd1R3D~ literal 0 HcmV?d00001 diff --git a/src/event_taxonomy/adapters/__pycache__/dep_risk.cpython-313.pyc b/src/event_taxonomy/adapters/__pycache__/dep_risk.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24709858d5def6fd48b24248d25a711f511e5b8b GIT binary patch literal 1367 zcmZ`(O>7%Q6rTO{|6i;G5h1cbXyZa+P}%}b1TCSciknpGDs510S=;qAS?ZlNGwT*R z0p&=zG?l27qmPL=gk#PXTJC5n>X)gX1j?* z6oLGFe$TotA@ql@3`FjO?iTe$9ZKGDE7Q0)n8nwp6 zsaRx%hwk)&O0ip~#1wodeM!I!aii`sSRbJ!bp7SkK1mmJ@toLGh;Ym22ck>5e9I3s zkR{<G9A}(t)5MKqVu>hT4)|P@!mBob&LNTNPlZbli z6LlO=PrHM1=|vv>zfD}cR%oUxxHjqACYfmvf@w1a7ECIbn2lh8%fzrT7cTP9JxrP9 zkh)(;?fJ$}8js$qS4r7)>cr)7+ac~MD>K)q;^y!TSi#h|ZB;F|p#_u+8ui=POuw)7 zxLUxypyd=E!KT9+%*A%!DKj04dB!#@;$mVD6PJ6YUJ5;|_~)~n%4OGasyu8OE?#kH zgNGT0XyIfYvN2=E3g$@)N7y#xX1I_A58K!^02?`pODou=ep=LjEjfv*FmWZg&Rn?5 z{JW-U8o!udb(l*X$IXL(;!e88w+?aaMxIf#XDYuif%BRRCCfuagXb|-Yw!peybL-9 zPDAjq-(c84op|;^VY9FuZ&eQB?`>Ru5__YQ80%!-ev%n$DF>NTo$ToL`G=o*L+`Xc zY~9@Xs{QUq-uTD+%0Aw|>7_3}iw!-P*__#0Ka7pHW8*uaz1VK-m&L=0x%R}|k%UI` zJNNh2cGvc2f6W}^3*OXK?`qL2RSrw5?b51e-S-;JgVLH;TyMYj-LpjISVbcxp{vM4 z(dWa(EuoW2Z?1P0loqIRWx8B~b_A8& zMP2ZW=$pR(@T~1r>Q($HJq@b=KVWBIIFbZGc#1~;M6*xP^pTnp)Gc>=u9e-s^6>j3 P1gdvt<{y8ir-1$+dc+JvmTLs{+aG_!+oIT){= zgafh19yK0}C;c;Mda#+m$$0Qq(|C7g`$0l9zGUC$&hO3p&1)r-eSpiyrBB}71OVSf z&=tAkv>tNj7+Ao9n_vSHI3fJRW@1AkQtV5c@`geb?#l(WpcUXkLg8mDrJyXxmUTG$E5#gNU0!-!AXC*x3ou3q(9Ba;Y39zFqPTko(9BTrVgxZMWm71krsG zI8eZJq@P=ff2{^r#~|OUS#TR{iOL1B5|*^cXBCnc*NkPG-(3WYo%1|aMEnm7_58oT z_R#)Q*Sq!Uc6(qOoG+bs)kW7iXc^1eO)v-A*6hn2T;W8eUE=N1w2LTmC?-sP&Y3wd zdKIR3nPi%e!q6@vChvMB#MF`(pa3&9jQpp#WNJ*N7?+st*c25piHKMPBG@DYTrpW3 zV7iZ}?b_5fB_b}yq#~k3Y=#Mm%1tQvnqaHhJseVkG0pO)3}~9#FL8kVC>s)|o!aT8 zIAEC~5@yBI#}j4B5i^4B!;@i-sR{6itJmPPf4q^LY@|ltjUA0uo>cSo)a}#MWcAkj zncCE1J+;&ryjC%4gHw&<(A)f*d}XAboap+a_2g7ze)-Gn=h?4&-{#k9^J|CGm9^@6 zZ9ILF%KW;l52#I`D{Au+$ic_(JkjFKLC!RoN~1F0Ln4-mO~&)}hRJk?$@G>uriTq* z_%C$!kZ-q1lwHrEOyY#aqkaHnghP`dfv?~9vAbVFE93?b1-s!4S4{~*cm_s(fR!_F dr>PoHJ*1Tf)vJ~DqgPG9!;_hNzlDvl{s71E?tTCO literal 0 HcmV?d00001 diff --git a/src/event_taxonomy/adapters/__pycache__/knowledge_silo.cpython-313.pyc b/src/event_taxonomy/adapters/__pycache__/knowledge_silo.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..85cb2a9e1723e07ff4c28b83eee011e6d2fd9d40 GIT binary patch literal 1352 zcmZ`&L2DaF6rR~#?MjwcisUo}H>#B2T9Lt4A#teVBv5Khv5nniqxv9$*hmv;lieM4 zcH~wnq&dbXyN7^!s-ZnJ^w1u2>`z$5hls%`hR{QAGN$*=$h#sfWDfJb_szU--n=(+ zs_PSgk6*9QK;ouoCfC1OR8YD0$u(U3%$wVGud0knHkO;?0MXhKR zcuk7%7$Z`NTvH5n8az};w4oKEe+$V1Wb&tOhns|BwyMPP9#w7gKC(HJ+Qh^$ZL5pw ztCm~0+y>Jg1VAN-wuxJVLL?YEw>)ofy2{ySpakx|y&7yXpdl?vXO0WYV*hBApdp9+ zm4GuR0Vu({;oSpP7#^vGuAvx_axC=9@eq~ZVsL(FOTbV?EfT=cIF1H5#_>de;~Y;0 zSic&5#~swS6h|sy)HrjVMW`CC&2w%G8i{vkCO-z@{f%=AvXNR+XFwsn$rRBPCU0zQ zv&fEVw^8@~M=t)_M)d}o_beOF*AYcEib*%|-vt(ZVA-f;(q=c2t7FGGxgCMnJqRHjgjZ5FNZJQ|pESkyy1h*-44GzWQ}*+5JYIkTvPs986u$%EtuqD*U= zo=GVoqQ69Rh=_N=WRH@9L{veDN8pL9ayhnIY+}BIF)i{VTslw9Z?KD>fD@R)5Dc#?G zrDghBW{{lP>+W`+Do62)1ATUox^$Gv{wyD?{1iJ(%?+ledpG)1vwk{%@c8$Je|xKc z{R@Bbj(^AS@0Iqvf~0@=0e1z_)NsI^hSyb%}cfZV0m>m4r!sDGl`Vd*$77 zZ~9Q5InK`d`A_=UrQ_s<{qJ5S^FtZr@_v4)pS|&7`;Th>R_SH7beu{LGBZDxe<=Tw zeR26PbL%9orYBBxeNr6)J))lS8-mS3jH$HK;vb)gf66Y6DmgCOLRhwiYS_lJp1k+J zeO|4J9COq(rqrz(B_D9PQ9Yjn@}a;Ng?{v=gX?V@-6k{K6`$9eO;@Q!p(^rQioD2^4fGr6drRWSXvIXP4Qjm>$Hd zC)-0Y#~v#l#FJP5goYjrLwgbr-Xg)fZ}L?viVyaE%$xbW_kR0YwmpDAJ}iIq?0qJUqg5~?ffp5g$_p`hL730wS#m98fmiW&N$HVKOFk_NW2@`vjPM;s zi&(;RWe}_aU#kJ?0Ok5Q7q6pdGIK%PgsUvUtWt8a&9Zuu3^6`vAe#MF->!W~1XJPa$sEP9-jLsn&yQ8px% zE!8k6VFrYIC69ZK!eoO&DHG1*hB%n%U}DMJw0NA_4r9*3kf-36Qa;bUmmv*IgFlS7oQ)rpg3A{ETQvi1Y61EYKfWll>LzwdLu3LSGrA-7`t#U zE^d>+>rknd{2~_$(AFOHJ!0bk%|<1@c{d14I~B6ZrXZ9Y$8(^X3dZ;hjeke0XXtiQ bx3Ip)YYz@D*PiUZY9a`a=T?46+s670IZgBA literal 0 HcmV?d00001 diff --git a/src/event_taxonomy/adapters/__pycache__/roadmap_entropy.cpython-313.pyc b/src/event_taxonomy/adapters/__pycache__/roadmap_entropy.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..edd88038e66b966883b2e5a66fa8ce565e3a7196 GIT binary patch literal 1052 zcmZ8f&ubGw6rTN=G(XyGgB6SD)*oPsZN;`qQ3^$?($F4UA_WV}c6XAjO?KnVL`@Ls z(R#6hLXSO(;=zNL9_xQl@lpo8C?33}1`pnRvq_5hU}wHJ^XB7y-@bM_oj?%pF1`1M z6@Unj z^kH;QCGk=$7yl_Ja}rnPf(C4vs#CLZz(`nY*ujC#7IJs;VBIUNcXdskCCR7Hp z)iqm%;`L_>QHREVLf0xQO1tf#TlRBY6sgMt}uri9;PnwYs?P=+bx5m+^7*h zBtC1{v|KM1E0~fbaOX6xvM{Xhxa%-n3Q2>mry=j{K=0rK9@Z&=^0ek@)W6q;62&;_=O5DzoeRu@qHHHbD z=z7$fnekeK_XWLqA;Z8OgO4_0Sx5V+?DPCaek-+&Kc~*G&woiC-%p>|H%@&qMz+s> zHqPx2n%hU;ocuI0vvYdSoZZ2@^ShV#GWi2NHq_tNl7op~DxgiNl|dO}<8eES(!)>Z zHt%mVpDiB9aCIPx4w6PnZy`g||NKfoPK9$H`sVsGACXv>a(!H9jT($H5%T#+r1((Y zcu(HA-Sajwk01H3j=E)Bbs~4U>iI6?3RoD)ev8_`jL0#;AHP}+y?O=Dl2ee1k*1@t qv=m8_zM`QYX!a|bZ0T7^-(*|YwzFHeUOZ_bNcTps{1!VIf%3lzTOYXq literal 0 HcmV?d00001 diff --git a/src/event_taxonomy/adapters/__pycache__/schema_evolution.cpython-313.pyc b/src/event_taxonomy/adapters/__pycache__/schema_evolution.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee7c719c2c214e37e7b08703c56602630e7da3d0 GIT binary patch literal 1168 zcmZ`&TWb?R6rR0gCu!{^@zUChq&8L)Xd>8F#0z*qY6}%y0zwL5O~!OJyAx(7)hI|G z>yyocG>>^yeDJTdmEgN)w!1~d9@w38zCGtVb1vH(80bSFpB6sbKU9Ri zNh1_JPax0|G0oStWqm2aBB1N5#wuP_=F|wpn32`UoNgLp z=$X!<6`YR#l$2>jXt&)497B#(D|)3h;#fsyd(Q!iuTbV#Rr?hw-?!be?N$VS8Yl)M zyw2Q`B3S~F*Vo|bB>{bp^61guUjrvoxs2waJSnbHkp8~p6;loIO95t$A(U4hg~bP` zJY2PaLlYvk9GVZ9T$F;zp0ieY-S#LGeR4|0Dp@C05j$kDszeMjQlgGS+_F!cR+$_NlXp)WLg$oLMYLpb zQlYFNq8`LEn>Pe@i04@q!iK<8L>$^tt{3N~#`~ literal 0 HcmV?d00001 diff --git a/src/event_taxonomy/adapters/__pycache__/semantic_diff.cpython-313.pyc b/src/event_taxonomy/adapters/__pycache__/semantic_diff.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..97964f4bad5e18c51f9e845ba77ebbfde7ba523f GIT binary patch literal 1441 zcmZ`(&rcgi6rT0|1bZDeZYYE_vT9Ukqk>f>sni0AT5N)W*eaV&Oti=IaO@xmesd{L8>oI= z8n?pU6K{Qh!Hx;_Np#ZQPoe02UkGUuxI=OsV*Evo6{5b&U&?L@zA?W)AEnR@VIChqx z*$dFp`N&}=17~s#>;Epw<(K82N9)s>TaiKY*7_H(^w!?aI&&|oLz&UyI)4eD2~F04 zmZUwB1b1RR{x3l%dohCEQ;zkX7}dZTXltOw%$UWgm;sa=d4j ziKTpDq42m^F5yI|74oYy#f8F=Nl-^O$*?8+mSeccu8f)X+8Sm)#zLiLxK#^_{-)y? zu)ZZthYmB1xeWu=R2EXQAnifeM+Q~E9P+$%EJf+mvE-k&v19qZLBers_PT{7$3li_ zAVZZfTeT3RBw*oDAz#!VVQ#kga2B%*<*zYUEX|Z5tup4Gc($twkTwYB$Ql0I1TbHz zk;efC$(m0A=}>SXSMz)XJrCtbCN3I7#sf#4Efh_3fdR(khF2*a|_!!`)wS9d2G6V31T7d!D=+xZ}o+MRFAx5a&>lgMlrg5PNNx^w-V zb!BIm+LNuxhV-Tj{6TsZzY)kc{>o&VslB1rPC-;1~6?c%}Eui4)} z`Ym_($(M)O>CQktkl))~ZY;M4J90W0Nx#hhp%nj-#e{GSL|!-{w*w_o9Rbxh$d3f+ zH-oQ*B%U%h?pJE0ZNl<&E6AC41!0y@9uoBvfTLs|{E%*zCLPb*T(|DS+r*>~#Ltr9 mm}MB|PcZm5m^u~)8KH^V)BCCRZ0q}DK+MD26aUgV4D>&xi9M14 literal 0 HcmV?d00001 diff --git a/src/event_taxonomy/adapters/__pycache__/test_flakiness.cpython-313-pytest-9.0.2.pyc b/src/event_taxonomy/adapters/__pycache__/test_flakiness.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f7312965393e7c6af8039490ec58f257f9f048b GIT binary patch literal 1295 zcmZ`&&ube;6rRC)-8Yvn7NRk)M9jLhN|&;4Jx!4e|@+n9qJ*u!o(Aaq~| zEZh*wTocxI+5@8;qh-v>1G#eJ${z^lW&W`G z#19zteO8539@}8}E8p|GVKtzwbI6`UkhjMO4XUv`a}6uKkf*#^H_{Sh*5C`i2lEt7 z(uJtFK2BeL`rvtbb&|b0$-nbFzdn3$k~b&C(!rf^@k&&!3KWr0QLzdoUxM(2r{)mXW|#q+V~H^3Dhqq?HmP`4{8Fs( zT+qr)(26}P&cb2(_&nPqQy%8i0BHWrpqF~ez)tl`!4y2UW2K4 zy+IYssVqs-?+AT^enl5vpnJceo2Tlcq<+f|J{}eiKKbtJQv}oK-4Fg2K2G=#zj9#T literal 0 HcmV?d00001 diff --git a/src/event_taxonomy/adapters/_severity.py b/src/event_taxonomy/adapters/_severity.py new file mode 100644 index 0000000..537e632 --- /dev/null +++ b/src/event_taxonomy/adapters/_severity.py @@ -0,0 +1,85 @@ +"""Centralized severity mapping logic for all tool representations.""" + +from event_taxonomy.schema import Severity + +# bus-factor / dep-risk / knowledge-silo style: CRITICAL/HIGH/MEDIUM/LOW(/OK) +_LABEL_MAP: dict[str, Severity] = { + "CRITICAL": Severity.CRITICAL, + "HIGH": Severity.HIGH, + "MEDIUM": Severity.MEDIUM, + "LOW": Severity.LOW, + "OK": Severity.INFO, +} + + +def map_risk_label(label: str) -> Severity: + """Map CRITICAL/HIGH/MEDIUM/LOW/OK string labels.""" + return _LABEL_MAP.get(label.upper(), Severity.INFO) + + +# doc-drift style: error/warning/info +_DOC_MAP: dict[str, Severity] = { + "error": Severity.HIGH, + "warning": Severity.MEDIUM, + "info": Severity.INFO, +} + + +def map_doc_severity(sev: str) -> Severity: + """Map error/warning/info string labels.""" + return _DOC_MAP.get(sev.lower(), Severity.INFO) + + +# perf-regression style: integer 1-10 +def map_int_severity(score: int) -> Severity: + """Map integer severity (1-10 scale).""" + if score >= 8: + return Severity.CRITICAL + if score >= 6: + return Severity.HIGH + if score >= 4: + return Severity.MEDIUM + if score >= 2: + return Severity.LOW + return Severity.INFO + + +# schema-evolution style: safe/cautious/dangerous +_SCHEMA_MAP: dict[str, Severity] = { + "dangerous": Severity.CRITICAL, + "cautious": Severity.MEDIUM, + "safe": Severity.LOW, +} + + +def map_schema_risk(level: str) -> Severity: + """Map safe/cautious/dangerous string labels.""" + return _SCHEMA_MAP.get(level.lower(), Severity.INFO) + + +# roadmap-entropy style: critical/high/moderate/low +_ENTROPY_MAP: dict[str, Severity] = { + "critical": Severity.CRITICAL, + "high": Severity.HIGH, + "moderate": Severity.MEDIUM, + "low": Severity.LOW, +} + + +def map_entropy_risk(label: str) -> Severity: + """Map critical/high/moderate/low string labels.""" + return _ENTROPY_MAP.get(label.lower(), Severity.INFO) + + +# test-flakiness style: float 0.0-1.0 +def map_flakiness_rate(rate: float) -> Severity: + """Map flakiness rate (0.0-1.0) to severity.""" + if rate >= 0.5: + return Severity.CRITICAL + if rate >= 0.3: + return Severity.HIGH + if rate >= 0.15: + return Severity.MEDIUM + if rate > 0.0: + return Severity.LOW + return Severity.INFO diff --git a/src/event_taxonomy/adapters/bus_factor.py b/src/event_taxonomy/adapters/bus_factor.py new file mode 100644 index 0000000..8de8dec --- /dev/null +++ b/src/event_taxonomy/adapters/bus_factor.py @@ -0,0 +1,20 @@ +from typing import Any + +from event_taxonomy.adapters._severity import map_risk_label +from event_taxonomy.schema import NormalizedFinding + + +def normalize(finding: dict[str, Any]) -> NormalizedFinding: + return NormalizedFinding( + tool="bus-factor-analyzer", + category="bus-factor", + severity=map_risk_label(finding["risk_label"]), + message=f"Bus factor {finding['bus_factor']} — {finding['top_author']} owns {finding['top_author_pct']:.0f}%", + file=finding.get("file"), + metadata={ + "top_author": finding["top_author"], + "top_author_pct": finding["top_author_pct"], + "num_contributors": finding["num_contributors"], + "bus_factor": finding["bus_factor"], + }, + ) diff --git a/src/event_taxonomy/adapters/dep_risk.py b/src/event_taxonomy/adapters/dep_risk.py new file mode 100644 index 0000000..8f02267 --- /dev/null +++ b/src/event_taxonomy/adapters/dep_risk.py @@ -0,0 +1,22 @@ +from typing import Any + +from event_taxonomy.adapters._severity import map_risk_label +from event_taxonomy.schema import NormalizedFinding + + +def normalize(finding: dict[str, Any]) -> NormalizedFinding: + label = finding.get("risk_label", "OK") + return NormalizedFinding( + tool="dep-risk-scanner", + category="dependency-risk", + severity=map_risk_label(label), + message=f"{finding['name']}@{finding['version']} — {label}", + recommendation=f"Vulnerability count: {finding.get('vuln_count', 0)}, months stale: {finding.get('months_stale', 0)}" + if finding.get("vuln_count") or finding.get("months_stale") + else None, + metadata={ + k: finding[k] + for k in ("ecosystem", "vuln_score", "maintenance_score", "risk_score", "vulns") + if k in finding + }, + ) diff --git a/src/event_taxonomy/adapters/doc_drift.py b/src/event_taxonomy/adapters/doc_drift.py new file mode 100644 index 0000000..6fa6d58 --- /dev/null +++ b/src/event_taxonomy/adapters/doc_drift.py @@ -0,0 +1,16 @@ +from typing import Any + +from event_taxonomy.adapters._severity import map_doc_severity +from event_taxonomy.schema import NormalizedFinding + + +def normalize(finding: dict[str, Any]) -> NormalizedFinding: + return NormalizedFinding( + tool="doc-drift-detector", + category=finding.get("kind", "doc-drift"), + severity=map_doc_severity(finding["severity"]), + message=finding["message"], + file=finding.get("file"), + line=finding.get("lineno"), + metadata={"symbol": finding["symbol"]} if finding.get("symbol") else {}, + ) diff --git a/src/event_taxonomy/adapters/knowledge_silo.py b/src/event_taxonomy/adapters/knowledge_silo.py new file mode 100644 index 0000000..8b16ffc --- /dev/null +++ b/src/event_taxonomy/adapters/knowledge_silo.py @@ -0,0 +1,24 @@ +from typing import Any + +from event_taxonomy.adapters._severity import map_risk_label +from event_taxonomy.schema import NormalizedFinding + + +def normalize(finding: dict[str, Any]) -> NormalizedFinding: + risk = finding.get("risk", "LOW") + if hasattr(risk, "value"): + risk = risk.value + return NormalizedFinding( + tool="knowledge-silo-detector", + category="knowledge-silo", + severity=map_risk_label(str(risk)), + message=f"{finding['filepath']} dominated by {finding['dominant_author']} ({finding['dominant_commits']}/{finding['total_commits']} commits)", + file=finding.get("filepath"), + metadata={ + "dominant_author": finding["dominant_author"], + "dominance_ratio": finding["dominant_commits"] / finding["total_commits"] + if finding["total_commits"] + else 0, + "other_authors": finding.get("other_authors", {}), + }, + ) diff --git a/src/event_taxonomy/adapters/perf_regression.py b/src/event_taxonomy/adapters/perf_regression.py new file mode 100644 index 0000000..172d0a9 --- /dev/null +++ b/src/event_taxonomy/adapters/perf_regression.py @@ -0,0 +1,16 @@ +from typing import Any + +from event_taxonomy.adapters._severity import map_int_severity +from event_taxonomy.schema import NormalizedFinding + + +def normalize(finding: dict[str, Any]) -> NormalizedFinding: + return NormalizedFinding( + tool="perf-regression-spotter", + category=finding.get("pattern", "performance"), + severity=map_int_severity(finding["severity"]), + message=finding["message"], + file=finding.get("file"), + line=finding.get("line"), + metadata={"snippet": finding["snippet"]} if finding.get("snippet") else {}, + ) diff --git a/src/event_taxonomy/adapters/roadmap_entropy.py b/src/event_taxonomy/adapters/roadmap_entropy.py new file mode 100644 index 0000000..b1790b4 --- /dev/null +++ b/src/event_taxonomy/adapters/roadmap_entropy.py @@ -0,0 +1,26 @@ +from typing import Any + +from event_taxonomy.adapters._severity import map_entropy_risk +from event_taxonomy.schema import NormalizedFinding + + +def normalize(finding: dict[str, Any]) -> NormalizedFinding: + label = finding.get("risk_label", "low") + return NormalizedFinding( + tool="roadmap-entropy", + category="roadmap-entropy", + severity=map_entropy_risk(label), + message=f"Entropy score {finding['entropy_score']:.2f} — {label}", + metadata={ + k: finding[k] + for k in ( + "base_count", + "head_count", + "item_count_delta", + "description_churn", + "priority_shuffles", + "entropy_score", + ) + if k in finding + }, + ) diff --git a/src/event_taxonomy/adapters/schema_evolution.py b/src/event_taxonomy/adapters/schema_evolution.py new file mode 100644 index 0000000..b8a58c4 --- /dev/null +++ b/src/event_taxonomy/adapters/schema_evolution.py @@ -0,0 +1,22 @@ +from typing import Any + +from event_taxonomy.adapters._severity import map_schema_risk +from event_taxonomy.schema import NormalizedFinding + + +def normalize(finding: dict[str, Any]) -> NormalizedFinding: + op = finding.get("operation", {}) + if hasattr(op, "op_type"): + op_type = op.op_type + op_args = getattr(op, "args", []) + else: + op_type = op.get("op_type", "unknown") + op_args = op.get("args", []) + return NormalizedFinding( + tool="schema-evolution-advisor", + category=op_type, + severity=map_schema_risk(finding["risk_level"]), + message=finding["rationale"], + recommendation=finding.get("recommendation"), + metadata={"op_type": op_type, "args": op_args}, + ) diff --git a/src/event_taxonomy/adapters/semantic_diff.py b/src/event_taxonomy/adapters/semantic_diff.py new file mode 100644 index 0000000..e0d2786 --- /dev/null +++ b/src/event_taxonomy/adapters/semantic_diff.py @@ -0,0 +1,28 @@ +from typing import Any + +from event_taxonomy.schema import NormalizedFinding, Severity + + +def normalize(finding: dict[str, Any]) -> NormalizedFinding: + cats = finding.get("categories", []) + added = finding.get("added", 0) + removed = finding.get("removed", 0) + churn = added + removed + if "DELETION" in cats or "DELETED_FILE" in cats: + severity = Severity.MEDIUM + elif churn > 100: + severity = Severity.HIGH + elif churn > 30: + severity = Severity.MEDIUM + elif churn > 0: + severity = Severity.LOW + else: + severity = Severity.INFO + return NormalizedFinding( + tool="semantic-diff", + category=",".join(cats) if cats else "change", + severity=severity, + message=finding.get("summary", f"Changed {finding['path']}"), + file=finding.get("path"), + metadata={"added": added, "removed": removed, "categories": cats}, + ) diff --git a/src/event_taxonomy/adapters/test_flakiness.py b/src/event_taxonomy/adapters/test_flakiness.py new file mode 100644 index 0000000..13bd647 --- /dev/null +++ b/src/event_taxonomy/adapters/test_flakiness.py @@ -0,0 +1,23 @@ +from typing import Any + +from event_taxonomy.adapters._severity import map_flakiness_rate +from event_taxonomy.schema import NormalizedFinding + + +def normalize(finding: dict[str, Any]) -> NormalizedFinding: + rate = finding["flakiness_rate"] + return NormalizedFinding( + tool="test-flakiness-analyzer", + category="flaky-test", + severity=map_flakiness_rate(rate), + message=f"{finding['test_id']} flaky at {rate:.0%} ({finding['fail_count']}/{finding['total_runs']} failures)", + metadata={ + "test_id": finding["test_id"], + "classname": finding.get("classname", ""), + "total_runs": finding["total_runs"], + "pass_count": finding.get("pass_count", 0), + "fail_count": finding["fail_count"], + "flakiness_rate": rate, + "avg_duration": finding.get("avg_duration", 0), + }, + ) diff --git a/src/event_taxonomy/registry.py b/src/event_taxonomy/registry.py new file mode 100644 index 0000000..72dbf92 --- /dev/null +++ b/src/event_taxonomy/registry.py @@ -0,0 +1,48 @@ +"""Adapter registry — maps tool names to normalize functions.""" + +from typing import Any, Callable + +from event_taxonomy.adapters import ( + bus_factor, + dep_risk, + doc_drift, + knowledge_silo, + perf_regression, + roadmap_entropy, + schema_evolution, + semantic_diff, + test_flakiness, +) +from event_taxonomy.schema import NormalizedFinding, ToolEvent + +_ADAPTERS: dict[str, Callable[[dict[str, Any]], NormalizedFinding]] = { + "bus-factor-analyzer": bus_factor.normalize, + "dep-risk-scanner": dep_risk.normalize, + "doc-drift-detector": doc_drift.normalize, + "knowledge-silo-detector": knowledge_silo.normalize, + "perf-regression-spotter": perf_regression.normalize, + "roadmap-entropy": roadmap_entropy.normalize, + "schema-evolution-advisor": schema_evolution.normalize, + "semantic-diff": semantic_diff.normalize, + "test-flakiness-analyzer": test_flakiness.normalize, +} + + +def list_tools() -> list[str]: + return sorted(_ADAPTERS.keys()) + + +def get_adapter(tool_name: str) -> Callable[[dict[str, Any]], NormalizedFinding]: + if tool_name not in _ADAPTERS: + raise KeyError(f"Unknown tool: {tool_name}. Available: {', '.join(sorted(_ADAPTERS))}") + return _ADAPTERS[tool_name] + + +def normalize_output( + tool_name: str, + raw_findings: list[dict[str, Any]], + tool_version: str = "unknown", +) -> ToolEvent: + adapter = get_adapter(tool_name) + findings = [adapter(f) for f in raw_findings] + return ToolEvent(tool_name=tool_name, tool_version=tool_version, findings=findings) diff --git a/src/event_taxonomy/schema.py b/src/event_taxonomy/schema.py new file mode 100644 index 0000000..c7d88c0 --- /dev/null +++ b/src/event_taxonomy/schema.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import enum +import json +from dataclasses import asdict, dataclass, field +from datetime import datetime, timezone +from typing import Any + + +class Severity(enum.IntEnum): + """Canonical severity levels, ordered from most to least severe.""" + + CRITICAL = 5 + HIGH = 4 + MEDIUM = 3 + LOW = 2 + INFO = 1 + + def __str__(self) -> str: + return self.name + + +@dataclass +class NormalizedFinding: + """Unified finding representation across all analysis tools.""" + + tool: str + category: str + severity: Severity + message: str + file: str | None = None + line: int | None = None + recommendation: str | None = None + metadata: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + d = asdict(self) + d["severity"] = self.severity.name + return d + + +@dataclass +class ToolEvent: + """Envelope wrapping a tool's normalized output.""" + + tool_name: str + tool_version: str + timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat()) + findings: list[NormalizedFinding] = field(default_factory=list) + + @property + def summary(self) -> dict[str, int]: + counts: dict[str, int] = {s.name: 0 for s in Severity} + for f in self.findings: + counts[f.severity.name] += 1 + return {"total": len(self.findings), **counts} + + def to_dict(self) -> dict[str, Any]: + return { + "tool_name": self.tool_name, + "tool_version": self.tool_version, + "timestamp": self.timestamp, + "findings": [f.to_dict() for f in self.findings], + "summary": self.summary, + } + + def to_json(self, **kwargs: Any) -> str: + return json.dumps(self.to_dict(), **kwargs) diff --git a/tests/__pycache__/test_adapters.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_adapters.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f0f63658fc8810ac338f9cbd9f94a671a33e7e4 GIT binary patch literal 20387 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 Yh6HZvLDb8QJctS07am4~oav;M1& literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_schema.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_schema.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a6efb98ac4fa978583ba686621558f56e91c2ed GIT binary patch literal 13667 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 J`oJh}_`mO|@G}4a literal 0 HcmV?d00001 diff --git a/tests/test_adapters.py b/tests/test_adapters.py new file mode 100644 index 0000000..a57b575 --- /dev/null +++ b/tests/test_adapters.py @@ -0,0 +1,145 @@ +from event_taxonomy.adapters import ( + bus_factor, + dep_risk, + doc_drift, + knowledge_silo, + perf_regression, + roadmap_entropy, + schema_evolution, + semantic_diff, + test_flakiness, +) +from event_taxonomy.schema import Severity + + +def test_bus_factor(): + f = bus_factor.normalize({ + "file": "core.py", + "top_author": "alice", + "top_author_pct": 85.0, + "num_contributors": 2, + "bus_factor": 1, + "risk_label": "CRITICAL", + }) + assert f.severity == Severity.CRITICAL + assert f.file == "core.py" + assert "alice" in f.message + + +def test_dep_risk(): + f = dep_risk.normalize({ + "name": "requests", + "ecosystem": "pypi", + "version": "2.28.0", + "risk_label": "HIGH", + "vuln_count": 2, + "months_stale": 6, + }) + assert f.severity == Severity.HIGH + assert "requests" in f.message + + +def test_doc_drift(): + f = doc_drift.normalize({ + "kind": "docstring_param", + "severity": "error", + "message": "Param x not in signature", + "file": "model.py", + "lineno": 42, + "symbol": "process", + }) + assert f.severity == Severity.HIGH + assert f.line == 42 + assert f.file == "model.py" + + +def test_knowledge_silo(): + f = knowledge_silo.normalize({ + "filepath": "auth.py", + "total_commits": 50, + "dominant_author": "bob", + "dominant_commits": 45, + "other_authors": {"alice": 5}, + "risk": "HIGH", + }) + assert f.severity == Severity.HIGH + assert "bob" in f.message + + +def test_perf_regression(): + f = perf_regression.normalize({ + "pattern": "n-plus-one-query", + "severity": 8, + "file": "api.py", + "line": 123, + "message": "DB call inside loop", + "snippet": "for x in items: db.query(x)", + }) + assert f.severity == Severity.CRITICAL + assert f.file == "api.py" + assert f.line == 123 + + +def test_roadmap_entropy(): + f = roadmap_entropy.normalize({ + "entropy_score": 0.65, + "risk_label": "high", + "base_count": 10, + "head_count": 15, + "item_count_delta": 5, + "description_churn": 0.25, + "priority_shuffles": 3, + }) + assert f.severity == Severity.HIGH + assert "0.65" in f.message + + +def test_schema_evolution(): + f = schema_evolution.normalize({ + "operation": {"op_type": "drop_column", "args": ["users", "password"]}, + "risk_level": "dangerous", + "rationale": "Dropping column causes data loss", + "recommendation": "Back up first", + }) + assert f.severity == Severity.CRITICAL + assert f.recommendation == "Back up first" + + +def test_semantic_diff(): + f = semantic_diff.normalize({ + "path": "api.py", + "categories": ["REFACTOR", "FEATURE"], + "summary": "Modified api.py", + "added": 45, + "removed": 23, + }) + assert f.severity == Severity.MEDIUM + assert f.file == "api.py" + + +def test_semantic_diff_large_churn(): + f = semantic_diff.normalize({ + "path": "big.py", + "categories": ["FEATURE"], + "summary": "Rewrote big.py", + "added": 200, + "removed": 50, + }) + assert f.severity == Severity.HIGH + + +def test_test_flakiness(): + f = test_flakiness.normalize({ + "test_id": "tests.integration::test_auth", + "classname": "tests.integration", + "total_runs": 20, + "pass_count": 12, + "fail_count": 8, + "error_count": 0, + "skip_count": 0, + "flakiness_rate": 0.4, + "avg_duration": 1.25, + "duration_stddev": 0.35, + }) + assert f.severity == Severity.HIGH + assert "40%" in f.message diff --git a/tests/test_schema.py b/tests/test_schema.py new file mode 100644 index 0000000..03b1211 --- /dev/null +++ b/tests/test_schema.py @@ -0,0 +1,52 @@ +import json + +from event_taxonomy.schema import NormalizedFinding, Severity, ToolEvent + + +def test_severity_ordering(): + assert Severity.CRITICAL > Severity.HIGH > Severity.MEDIUM > Severity.LOW > Severity.INFO + + +def test_severity_str(): + assert str(Severity.CRITICAL) == "CRITICAL" + assert str(Severity.INFO) == "INFO" + + +def test_finding_construction(): + f = NormalizedFinding( + tool="test-tool", + category="test", + severity=Severity.HIGH, + message="something broke", + file="foo.py", + line=42, + ) + assert f.tool == "test-tool" + assert f.severity == Severity.HIGH + assert f.file == "foo.py" + assert f.line == 42 + assert f.metadata == {} + assert f.recommendation is None + + +def test_finding_to_dict(): + f = NormalizedFinding(tool="t", category="c", severity=Severity.LOW, message="m") + d = f.to_dict() + assert d["severity"] == "LOW" + assert d["tool"] == "t" + + +def test_tool_event_serialization(): + findings = [ + NormalizedFinding(tool="t", category="c", severity=Severity.HIGH, message="a"), + NormalizedFinding(tool="t", category="c", severity=Severity.LOW, message="b"), + ] + event = ToolEvent(tool_name="t", tool_version="1.0", findings=findings) + d = event.to_dict() + assert d["summary"]["total"] == 2 + assert d["summary"]["HIGH"] == 1 + assert d["summary"]["LOW"] == 1 + j = event.to_json() + parsed = json.loads(j) + assert parsed["tool_name"] == "t" + assert len(parsed["findings"]) == 2 diff --git a/tests/test_severity.py b/tests/test_severity.py new file mode 100644 index 0000000..50af96f --- /dev/null +++ b/tests/test_severity.py @@ -0,0 +1,60 @@ +from event_taxonomy.adapters._severity import ( + map_doc_severity, + map_entropy_risk, + map_flakiness_rate, + map_int_severity, + map_risk_label, + map_schema_risk, +) +from event_taxonomy.schema import Severity + + +def test_risk_label_mapping(): + assert map_risk_label("CRITICAL") == Severity.CRITICAL + assert map_risk_label("high") == Severity.HIGH + assert map_risk_label("Medium") == Severity.MEDIUM + assert map_risk_label("LOW") == Severity.LOW + assert map_risk_label("OK") == Severity.INFO + assert map_risk_label("unknown") == Severity.INFO + + +def test_doc_severity_mapping(): + assert map_doc_severity("error") == Severity.HIGH + assert map_doc_severity("warning") == Severity.MEDIUM + assert map_doc_severity("info") == Severity.INFO + assert map_doc_severity("ERROR") == Severity.HIGH + + +def test_int_severity_boundaries(): + assert map_int_severity(10) == Severity.CRITICAL + assert map_int_severity(8) == Severity.CRITICAL + assert map_int_severity(7) == Severity.HIGH + assert map_int_severity(6) == Severity.HIGH + assert map_int_severity(5) == Severity.MEDIUM + assert map_int_severity(4) == Severity.MEDIUM + assert map_int_severity(3) == Severity.LOW + assert map_int_severity(2) == Severity.LOW + assert map_int_severity(1) == Severity.INFO + + +def test_schema_risk_mapping(): + assert map_schema_risk("dangerous") == Severity.CRITICAL + assert map_schema_risk("cautious") == Severity.MEDIUM + assert map_schema_risk("safe") == Severity.LOW + assert map_schema_risk("Dangerous") == Severity.CRITICAL + + +def test_entropy_risk_mapping(): + assert map_entropy_risk("critical") == Severity.CRITICAL + assert map_entropy_risk("high") == Severity.HIGH + assert map_entropy_risk("moderate") == Severity.MEDIUM + assert map_entropy_risk("low") == Severity.LOW + + +def test_flakiness_rate_boundaries(): + assert map_flakiness_rate(0.0) == Severity.INFO + assert map_flakiness_rate(0.05) == Severity.LOW + assert map_flakiness_rate(0.15) == Severity.MEDIUM + assert map_flakiness_rate(0.3) == Severity.HIGH + assert map_flakiness_rate(0.5) == Severity.CRITICAL + assert map_flakiness_rate(1.0) == Severity.CRITICAL