From 5371fb8f41baf1c0a2afb3ded40657e183af19fb Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Thu, 30 Jul 2026 01:53:16 -0400 Subject: [PATCH 1/3] =?UTF-8?q?fix(vendor-graph):=20track-minor=20was=20tr?= =?UTF-8?q?ack-latest=20=E2=80=94=20follow=20the=20pinned=20x.y=20line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `track-minor` was implemented with `majorOf`: const sameMajorNewer = intervening.some((a) => majorOf(a.version) === majorOf(pinnedVersion)) const majorMoved = releaseDistance > 0 && majorOf(latestVersion) !== majorOf(pinnedVersion) freshnessState = sameMajorNewer || majorMoved ? 'stale' : 'current' A policy whose NAME states one contract and whose CODE honours another. And it is worse than "track-minor is really track-major": the two arms are EXHAUSTIVE — if nothing newer shares the pinned major, the newest release cannot either, so `majorMoved` fires. `releaseDistance > 0` was therefore always stale, which is `track-latest`. Three declared policies, two distinct behaviours, and the collapsed one is the DEFAULT that every pin in the live vendor register declares. Consequence, measured before the change: a pin at 0.4.46 was reported stale against 0.5.0, and `proposeRevendor` sealed an EffectRequest to take it. A manifest author who wrote `track-minor` to stay on a minor line was silently opted in to leaving it. Established semantics — a pin on `x.y.*` follows its own minor line: · 0.4.46 → 0.4.47 in-lane ⇒ stale, target 0.4.47 (accepted) · 0.4.46 → 0.5.0 off-lane ⇒ current (refused) · 0.4.46 → 1.0.0 off-lane ⇒ current (refused) `current` here never means "nothing happened upstream": the rationale names every off-line release and `releaseDistance`/`latestVersion` still report the real gap. Also fixes the same defect one layer down. `proposeRevendor` took `verdict.latestVersion`, so a correctly-stale track-minor pin at 0.4.46 with 0.4.47 AND 0.5.0 upstream would have proposed → 0.5.0: the exact bump the policy refuses, under a rationale saying it was withheld. `StalenessVerdict` now carries `policyTargetVersion` — the newest release the POLICY will take — and the proposal uses it. track-latest is unchanged and still proposes the newest release. Two boundary rules, stated so they are not rediscovered as bugs: · a pin that is not release-shaped (a sha, `unknown`) names no line, so its state is `unknown` — the same answer, for the same reason, that an unobserved pin gets · a prerelease does not make a stable pin stale (semver ranges treat prereleases as opt-in); a pin that is itself a prerelease does follow them The line is derived through this branch's `parseVersion`, not `split('.')`, so `v0.4.46`, `0.4.46+build.7` and `0.4` all resolve to the 0.4 line — which is why this stacks on #42 rather than duplicating a version parser. Proof: the 9 new tests fail against the `majorOf` implementation and pass against this one. 426/426 green, typecheck clean, ts/dist rebuilt. --- ts/dist/index.d.mts | Bin 232421 -> 229247 bytes ts/dist/index.d.ts | Bin 232421 -> 229247 bytes ts/dist/index.js | Bin 431113 -> 428271 bytes ts/dist/index.mjs | Bin 422119 -> 419411 bytes ts/src/vendor-graph.test.ts | 146 ++++++++++++++++++++++++++++++++++++ ts/src/vendor-graph.ts | 109 +++++++++++++++++++++++---- 6 files changed, 242 insertions(+), 13 deletions(-) diff --git a/ts/dist/index.d.mts b/ts/dist/index.d.mts index 0d4b92c2db743820fb1710fc32a17034868d15c6..f9a9e650f1b06adb91a0d90973a354dd9ac98ff2 100644 GIT binary patch delta 3105 zcmb_e&ud&&7$q6}Ra8(HQa66>qR9+1V}#m5N-ASJttn|zlC)AN`0l)SXXcsr-W~6~ zFOz}73se*rt{TWr?B0zajNn4hg)Rj*t_%JPU8?81_q}NnaU+J1z|6hhkMo^#zWc}F z%YXg((*Cz|zdo7XcxFEQdV24>c`-uWw=AJJw*+Yd!r-{W1CD!Si)fnMUs$- zt)xXGB?Y@OiXo|`8yd$+!bMs|R_sW6ynjIBVH^(mOH@rF8HQ4&!zsXw#MziS#fEHyJ%+Ujo*ehu{2i zy0fwFap0L;qV^W3(ou>e!1*zrn#28vC2WMbd<#1TjK;#?2%q8~cnTD3L@IBJkCa}A zqBX5hoY_KZa1Zin89r!Me+Z{-8lF0@84AkwCwov&9bJLDP5EIvLtmf8)wE_z# zVkgtsCy@*jv>AC%ilu21!xdr5aR!^{NoWns)F{6L4h--GNGEdGnx?Uiacls0YhbBW z^Zd~J$1)u=XHb#AY1`^Ls>fv`M(tOV`i!+FUh*@Km?elUKIM*tx@y705 zw>rb)DA<)b5??S9m`CtO4SRyKI{mJzF_&?Ck|&G8VsWJ$t;265j(g?y=4N+&`{a&3YZ7zf4GxIeOL<8NAeO{E2_nGyF-@KUY*39e%uq&dcj1R=Iz` z_%O-YVZ2T_m!En88Pf=^&Q|7@A#W8I+9g|H@eM`NXWn5kRm`LeW;WB9>i1i&_EIP4 zAe-Rb;RRKSJ|GqfGz!)@S2uJvD@x^p_#?@SJ|BVbQlw`q{I|jCp31XGfNMHWnCU&- zV!#~*@bVc-EJTitE}@Cogz!HW2PWLh2WFWeAo_6%De&mVLu`HCsHR}E2I9QG%Hk^( zE((Gku@>`eCj%Hk9qWr9Rpb}EYZwd4uhXVaLH-rw=z!TF^6a{|I6(uv_pmQAXFBkI zXqF!mTu3D~-V}h)@S4EH4E1wp8YikGA^}Et1q>iK2Ags`3k>fP7y?s*Im}abk#N?y zr@{<`Tbtd@?pn9ARdgv*2*JqlYXsB-u05=O#>sPf(QEHwONZ^Z@dyK9abqUPa=i5! z0(c>Z&W?v_3{9mxZ!NL`^97|?UYZqAwwDPG(BhIkG2#p?!K)cY=V#>}TQP4_x6fj0 zWYR~OPlNal{1~|Dl%LVlpRd>I_32-u+Ul#*dk<=@x#^dm)S|iRE1%ZBs!f0Tymof} EZ%px5-2eap delta 6140 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 diff --git a/ts/dist/index.d.ts b/ts/dist/index.d.ts index 0d4b92c2db743820fb1710fc32a17034868d15c6..f9a9e650f1b06adb91a0d90973a354dd9ac98ff2 100644 GIT binary patch delta 3105 zcmb_e&ud&&7$q6}Ra8(HQa66>qR9+1V}#m5N-ASJttn|zlC)AN`0l)SXXcsr-W~6~ zFOz}73se*rt{TWr?B0zajNn4hg)Rj*t_%JPU8?81_q}NnaU+J1z|6hhkMo^#zWc}F z%YXg((*Cz|zdo7XcxFEQdV24>c`-uWw=AJJw*+Yd!r-{W1CD!Si)fnMUs$- zt)xXGB?Y@OiXo|`8yd$+!bMs|R_sW6ynjIBVH^(mOH@rF8HQ4&!zsXw#MziS#fEHyJ%+Ujo*ehu{2i zy0fwFap0L;qV^W3(ou>e!1*zrn#28vC2WMbd<#1TjK;#?2%q8~cnTD3L@IBJkCa}A zqBX5hoY_KZa1Zin89r!Me+Z{-8lF0@84AkwCwov&9bJLDP5EIvLtmf8)wE_z# zVkgtsCy@*jv>AC%ilu21!xdr5aR!^{NoWns)F{6L4h--GNGEdGnx?Uiacls0YhbBW z^Zd~J$1)u=XHb#AY1`^Ls>fv`M(tOV`i!+FUh*@Km?elUKIM*tx@y705 zw>rb)DA<)b5??S9m`CtO4SRyKI{mJzF_&?Ck|&G8VsWJ$t;265j(g?y=4N+&`{a&3YZ7zf4GxIeOL<8NAeO{E2_nGyF-@KUY*39e%uq&dcj1R=Iz` z_%O-YVZ2T_m!En88Pf=^&Q|7@A#W8I+9g|H@eM`NXWn5kRm`LeW;WB9>i1i&_EIP4 zAe-Rb;RRKSJ|GqfGz!)@S2uJvD@x^p_#?@SJ|BVbQlw`q{I|jCp31XGfNMHWnCU&- zV!#~*@bVc-EJTitE}@Cogz!HW2PWLh2WFWeAo_6%De&mVLu`HCsHR}E2I9QG%Hk^( zE((Gku@>`eCj%Hk9qWr9Rpb}EYZwd4uhXVaLH-rw=z!TF^6a{|I6(uv_pmQAXFBkI zXqF!mTu3D~-V}h)@S4EH4E1wp8YikGA^}Et1q>iK2Ags`3k>fP7y?s*Im}abk#N?y zr@{<`Tbtd@?pn9ARdgv*2*JqlYXsB-u05=O#>sPf(QEHwONZ^Z@dyK9abqUPa=i5! z0(c>Z&W?v_3{9mxZ!NL`^97|?UYZqAwwDPG(BhIkG2#p?!K)cY=V#>}TQP4_x6fj0 zWYR~OPlNal{1~|Dl%LVlpRd>I_32-u+Ul#*dk<=@x#^dm)S|iRE1%ZBs!f0Tymof} EZ%px5-2eap delta 6140 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 diff --git a/ts/dist/index.js b/ts/dist/index.js index 10b5ec9ca248d578b973dec4e6a4216284075f0b..98da9f84838da2f4e48b98e111c71341e82be2a2 100644 GIT binary patch delta 2966 zcma)8eQX=$8P{w3F0I>igg7n=9ed-H=1#FMPFp%9CQYlYf0U*lDgBt{xcH6zI==RN zXYS6SoqG#NMHN%qwX&2~2r2;rR4A1|0Z!AjuHDckKu91YkkAJEV?vWAK7v-k{@R}R z?wr_Jk>)>6zUO(~=i~SMp69(MuJnF&srS__zny;?MH)MQ_j#!9ENs61Mkrp7V)zvd z-LkQ1Jc7q?-N{7R;_oA|&RJrrWLl$^jO)?H!^!SO@6AnM4|dtv93E>taI>F1{4>-Y z1xW@Jj+|kWF|>Z+!p%FwY~4lF(O<`kqD-rLQPpRXIBQe0qUfYTbECLS^rA+wvvBLJ zyF&|q`1AD8x>jpYjC?!NxkBciji5}}7oJRV=clm4CVzr9u*D8^0P1r0uR^aP{--mF z4xqT?IkgN8!t+EFC@VM0o?3(Y!jQ51(Hhi)?pQVgmlf>1*@>PO*{4^-B73eI-P4<$ zfr5GERyLg*SNQArIM!>L#{Slg?p>873bDY1b;VF@JqH8hplnO2)~vE*)NFT84Bd=% zwqjmki%*7d!+4`}{zgPdwLo4`ENV-$No;YjgJ81n*`#*O)Ekd|9P6lfBFEh!qmPlh zXAo@*b?HjgG^*6JRcd8V&{H}0d zubn}ej=TKMbK0rq5#DrMRdwF-%sF&4%&J!efqnLCv=)2K=XO?s=wNAO)tu5iQ^rnX z$BiC0auJ;t*>~H7AN7|E6HETg)M0jjStc!E2OA{}T|P)lmdswCM}w=3END3^{yC5C z@kT{7yg@k|XUL7S&=cM4nU_NucIl-HerY)YMQdFn6#=9D19h9Wlrf+c5ZM83P|9D9AJ`8;``ER_nozoq4CvJsZh%z zGxTg~BI2edgaL(ZdsMiwCJ97MKsgm+ySE+{J_xzj9~b@+VYy!j54e|qB^(W>)e@Gh zLYY=bHn2=UMPp+)Q8U#9-n0q#sn*VE7_gd651T3*JTKhME?*Gtj1wJ`K!Fb*+@1HS z%3eDX>0$r5D8$@vE(p$x3+Q5ZI?S_e!T9D^V{?AZ2| zvBrV_OGbTA`mCWAaXJ%S%G`3FzfZhBGN4f#R}CQLsYAq^p>~ijPhZt_S`6OZzl?}q zqyF1lYQ&~i^CbIIMttiI4KM*8OZc$iF>!qdFCR2)-KB7x-5L?`9PDPoktDEJ$8g>@ zN#R7wJLPd762P-Qf_MOcX-&x)04%U>z%}55n6k>GN{g_646NFiFKw>JCt>4y`Pc?7 z8JY%{##C){dUz|Y?bD02pceU^k@Q>@YNknb8%rS9h)FpLDSDC=Yz!mhr{|ZO=ZW~mMXq?A+*UL=WltzN}!(GTDD8Jw^hd5ahgwr)ROM$(|kadS+;yymRF`*D*aEs zTmR35IW2j@o=dhkBKCCp)him+XY4Y2CF2pqhf}AI0LS(>;qfiAZ3l1)55r9tAmZOU z{>Zf*ULFVedzjy1K1Z8={4@-|;C+V8n-shS342(M<2;r&*4s4><}S@9Q6%_2DU(@> zHCnQIgK}<@G;BPctxw>#q+xoz27#~zJ?bnW!4Y$K{Fa#P z%6749^81~(9mYUQfHnwYK$nT8@f=#0k0a-uJWEeRV(ypQ#nqwi+bk@1afkTH503I( zy8FTX5B+rKp>aS;ki*=mLI z^#-@Ir533NJFGX8??a-&#R;==G%%1x;b9tvPkhRqNA!?GzU%6Vuu#@>uM zOScn%$6lBeZ>(v7$31>f+#7d4EsH{r8~dgBuaKL0Oq>t9-<=U>R=ejeh>q}I6|VHQ delta 5517 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 Qk(W3wosD_FHKdgAfBw<@pa1{> delta 5305 zcmbVQYitzP71s572V@|G*oKCXUM>{m zcu%#dCS5Re!qvc4O-(gW<%z16rR8+#e^4C4Oh=pgA~ADZy(Ot3ywJSh3-q73qvavY zm9*VpEa?~2LYYW#cL-l3EAAay7E8Naiwd88;n?L$tm)N4GqMDkzP#~64R7;Xnk@5HuNND}*?>^GwnVqbJ>OX&X&&E_7 z@?4eWHT2%W04*IBx}UW1f>G8pcpNtqxuEG8P0v9hF#qEmNt>8OMOTX?1&lHHy z;9bM{)-L?yfvy@Dau+@mWlr3kTrs zgb47*K)^PpiB&Gx5NpQnhm~n>iJni6ua(|Uhx=Qtk^DHU-TwRNmb)BD|A_~B+ia7T zLwH4|&mvT~hkNT8Ey>QO2wg{=#Mc7oETVt>F1jb8lVuxhZ8O3$Si!l8VCP9@l>!lg zG|+!`quo3GQ!fjQ$t6{t2ZE>{^|zY`3PZRkJC6zIb)o+1p^bFcm99Z2Cnhi}{1mmrYf%b*+U88^~8K|gMApU$F}#D;5jUW4eWrdkBg_z#8vNtWrKvS@J6UHFWPidE3k zM2bTE!a4Nq?X6A5`LK%O8%9?>++oPDZrND(!O8o5vq502CpGf|$0ROkdb~bX+3DXR-p@}KNhwKs#kj`K2lK^WpzNhWU; zEu{>AnkHGV4X%R8fOC^%q2WQz)1}Mk;AX!cvNL+^GJ0qWZ;8Aqg4Rp7FC*vn<*w&? zAqUB;U2&F*Zh-vvu6D^NcIlI=UCt+01D_4o3Px^_jn(#v3SN}RQ3D#gQIOJkx3#;W zOB)%&qL)|L(^*5)?Sw5wWL8@dR<15vo-8viU{P#i1_p$llcFQMOxQr;S{@z8wb<~m z>`!cQc6M3B2QkP34l*;gYT8mPgvE%+1wE&I*^%MlL&JCigIIZ;*pMrR$N2H8cMi(M zs>r4$lq5bq_2t;y@sm%SJ~nqI@ij1aI07Ct_KLJY%rrUYTVG+b6(T{etOP~+tA7PI z?BgWnvu&>M)uXX|cph_xI=a$3)#PaK%qPo$MHU$?%i9AUSY0=l{ouJ1*|njXny1%$ z0^8~LZv{5=ik)gwB8`3t?WkF*uG!ji$QL(ve99S&jkeJIlLy^aQD-2h}pU9nE;XP_+e z(h5;cNF{rAlgFxx|HR|t<2c5Os>b(V#)Fn8EH1NI#iVh z*>|k@ea`zm*6}S(jLj;unS=5KJU#uqlsq_?k~?_hfv`fBNgA$W_VQrh8uNrN`o05M zwP5jO&uvmX5aW2VTwEZgR7^%wvKLdZvq6n9D#X5(cF0`|nn8aQM_Cx%m>j`TmH?_Q zqF#wfdZ+C12vc>ZH1fefON#)%#??b983ba7nCb>`m=lLb=`)LH`?dyogp)@ioIF-c zK9b_utr%Jc(g|so8mx!!q;}L-%LWwvXdG>ze|;mcaU1KD$ET*xB+ktpJ@LfRQ#15C zKkMFAgD5A(45WF`hBS*I41+39(ig{Y698}kb-Y}H_b!rwPOR$qG4IvK3=4ssCv0c2O*IYg zkYxywg(s)Z{XdbawE?a(RCu-=i$sU8+PvPg*Up1+kexj20&W#qjm%gW-iR%*5{jqI zR14ZjylExYY)toB!AJW&=PaPO*ZKj_-%d}}FqJP`BX zRMIB^$%!a0Y7pV4eb8`|w*gKk<44(l(HMRzE|gZH$H(5NEx(kCTB9JKO)c2^#X;Li6} zLs*BvST?;Z3%jO^nr@h59tq}Wfv6S%;Wj_2fL2w{5zEt^HI3q3FQ8WyVYjrTF_?dm zE%R|_S6JBI<(!HNzu8Kc62iy>(8B?oe&1}HJ@lsuL8hPW5w_CL6T%TRPp>^Ld>rJq zI+u { assert.equal(store.getNode(emptyRepo), undefined, 'no node was created for the empty repository id') assert.deepEqual(store.in(emptyRepo, VFP_EDGE.producedBy), [], 'and nothing points at it') }) + +// ─── 9 ─ `track-minor` means the MINOR line ──────────────────────────────────── + +/** + * `track-minor` was implemented with `majorOf`: + * + * const sameMajorNewer = intervening.some((a) => majorOf(a.version) === majorOf(pinnedVersion)) + * const majorMoved = releaseDistance > 0 && majorOf(latestVersion) !== majorOf(pinnedVersion) + * freshnessState = sameMajorNewer || majorMoved ? 'stale' : 'current' + * + * A policy whose NAME states one contract and whose CODE honoured another. Worse than + * "track-minor is really track-major": the two arms are exhaustive — if nothing newer shares the + * pinned major then the newest release cannot either, so `majorMoved` fires — which made + * `releaseDistance > 0` always stale. `track-minor` was `track-latest`, wearing a major-shaped + * rationale string. Three declared policies, two distinct behaviours, and the collapsed one is + * the DEFAULT that every pin in the live register declares. + * + * These tests fix the contract the name states: a pin on `x.y.*` follows its own minor line. + */ +const minorLineFixture = (releases: string[], pinned: string, policy = 'track-minor'): HellGraphStore => { + const store = new HellGraphStore(new AtomSpace(`vendor-graph-line-${pinned}-${releases.join('_')}-${policy}`, false)) + ingestVendorFreshness(store, { + manifest_id: 'vendor-freshness-minor-line', + sources: [{ + source_id: 'engine', + repo: 'SocioProphet/hellgraph', + artifact_kind: 'npm-tarball', + version_scheme: 'semver', + releases: releases.map((version) => ({ version })), + }], + artifacts: [{ + artifact_id: 'engine@probe', + source_id: 'engine', + consumer_repo: 'SocioProphet/prophet-platform', + consumer_app: 'apps/probe', + vendored_version: pinned, + freshness_policy: policy, + disposition: 'current', + }], + }) + return store +} +const LINE_PIN = vfpId.pin('engine@probe') + +test('track-minor ACCEPTS a patch bump inside the pinned minor: 0.4.46 → 0.4.47', () => { + const v = stalenessOf(minorLineFixture(['0.4.46', '0.4.47'], '0.4.46'), LINE_PIN) + assert.equal(v.freshnessState, 'stale', '0.4.47 is on the 0.4 line the pin follows — take it') + assert.equal(v.policyTargetVersion, '0.4.47') + assert.match(v.rationale, /on the pinned 0\.4 line, newest 0\.4\.47/) +}) + +test('track-minor REFUSES a minor bump inside the pinned major: 0.4.46 → 0.5.0', () => { + const v = stalenessOf(minorLineFixture(['0.4.46', '0.5.0'], '0.4.46'), LINE_PIN) + assert.equal(v.freshnessState, 'current', '0.5.0 left the 0.4 line — not this pin\'s to take') + assert.equal(v.releaseDistance, 1, 'and the distance is still REPORTED: current ≠ nothing happened') + assert.equal(v.policyTargetVersion, '0.4.46', 'so no cross-minor target is proposed') + assert.match(v.rationale, /0\.5\.0.*not followed under track-minor/) +}) + +test('track-minor is no longer track-latest — the two now disagree', () => { + // The differential the old implementation could not produce: identical graph, both policies. + const cases: [string[], string, string, string][] = [ + [['0.4.46', '0.4.47'], '0.4.46', 'stale', 'stale'], + [['0.4.46', '0.5.0'], '0.4.46', 'current', 'stale'], + [['0.4.46', '1.0.0'], '0.4.46', 'current', 'stale'], + [['0.4.46', '0.4.47', '0.5.0'], '0.4.46', 'stale', 'stale'], + ] + for (const [releases, pinned, wantMinor, wantLatest] of cases) { + assert.equal(stalenessOf(minorLineFixture(releases, pinned), LINE_PIN).freshnessState, wantMinor, + `track-minor over ${releases.join('→')}`) + assert.equal(stalenessOf(minorLineFixture(releases, pinned, 'track-latest'), LINE_PIN).freshnessState, wantLatest, + `track-latest over ${releases.join('→')}`) + } +}) + +test('a stale track-minor pin proposes the newest IN-LANE release, not the newest release', () => { + // 0.4.47 and 0.5.0 both exist. The pin IS stale — 0.4.47 is on its line — and the proposal it + // earns is `→ 0.4.47`. Proposing `→ 0.5.0` would hand a human the cross-minor bump the policy + // exists to withhold, under a rationale that says it was withheld. + const store = minorLineFixture(['0.4.46', '0.4.47', '0.5.0'], '0.4.46') + const v = stalenessOf(store, LINE_PIN) + assert.equal(v.freshnessState, 'stale') + assert.equal(v.latestVersion, '0.5.0', 'the newest release is still reported as such') + assert.equal(v.policyTargetVersion, '0.4.47', 'but the policy will only take the 0.4 line') + + const p = proposeRevendor(store, LINE_PIN, { requestedAt: '2026-07-30T00:00:00Z' }) + assert.equal(p.effectRequest.parameters['toVersion'], '0.4.47') + assert.equal(p.effectRequest.idempotencyKey, 'engine@probe@0.4.46->0.4.47') + assert.deepEqual(p.contractViolations, []) +}) + +test('track-latest still proposes the newest release — the split is per-policy, not global', () => { + const store = minorLineFixture(['0.4.46', '0.4.47', '0.5.0'], '0.4.46', 'track-latest') + const v = stalenessOf(store, LINE_PIN) + assert.equal(v.policyTargetVersion, '0.5.0') + assert.equal(proposeRevendor(store, LINE_PIN, { requestedAt: '2026-07-30T00:00:00Z' }) + .effectRequest.parameters['toVersion'], '0.5.0') +}) + +test('the minor line is parsed, not string-split: v-prefix, build metadata, missing patch', () => { + // `split('.')[0]` on `v0.4.46` yields `v0`; on `0.4.46+build.7` the LINE is still 0.4. These all + // name a point on the 0.4 line, and `parseVersion` is what knows that. + for (const [pinned, newer] of [['v0.4.46', '0.4.47'], ['0.4.46+build.7', 'v0.4.47+build.8'], ['0.4', '0.4.1']]) { + const v = stalenessOf(minorLineFixture([pinned!, newer!], pinned!), LINE_PIN) + assert.equal(v.freshnessState, 'stale', `${pinned} → ${newer} is an in-lane bump`) + } + // …and 1 / 1.0 / 1.0.0 are one lane, so 1.1.0 leaves it. + assert.equal(stalenessOf(minorLineFixture(['1', '1.1.0'], '1'), LINE_PIN).freshnessState, 'current') + assert.equal(stalenessOf(minorLineFixture(['1', '1.0.1'], '1'), LINE_PIN).freshnessState, 'stale') +}) + +test('a prerelease does not make a stable track-minor pin stale — but does for a prerelease pin', () => { + assert.equal(stalenessOf(minorLineFixture(['0.4.46', '0.4.47-rc.1'], '0.4.46'), LINE_PIN).freshnessState, + 'current', 'prereleases are opt-in, as in every semver range') + assert.equal(stalenessOf(minorLineFixture(['0.4.47-rc.1', '0.4.47-rc.2'], '0.4.47-rc.1'), LINE_PIN).freshnessState, + 'stale', 'a pin that is itself a prerelease follows them') + assert.equal(stalenessOf(minorLineFixture(['0.4.47-rc.1', '0.4.47'], '0.4.47-rc.1'), LINE_PIN).freshnessState, + 'stale', 'and the release the prerelease led to is in-lane') +}) + +test('a track-minor pin that is not release-shaped is UNKNOWN, not a guess', () => { + // A sha names no x.y line. Deriving `current` would be the same silent guess the unobserved + // branch already refuses to make. + const store = new HellGraphStore(new AtomSpace('vendor-graph-shapeless', false)) + ingestVendorFreshness(store, { + manifest_id: 'vendor-freshness-shapeless', + sources: [{ source_id: 'engine', repo: 'SocioProphet/hellgraph', releases: [{ version: '7d74db8' }, { version: '0.4.47' }] }], + artifacts: [{ + artifact_id: 'engine@probe', source_id: 'engine', + consumer_repo: 'SocioProphet/prophet-platform', consumer_app: 'apps/probe', + vendored_version: '7d74db8', freshness_policy: 'track-minor', disposition: 'current', + }], + }) + const v = stalenessOf(store, LINE_PIN) + assert.equal(v.freshnessState, 'unknown') + assert.equal(v.dispositionAgrees, false, 'declaring `current` over an undecidable state is a violation') + assert.match(v.rationale, /not release-shaped/) +}) + +test('the live register is unaffected: 0.4.40 → 0.4.45 is all one 0.4 line', () => { + // The fix must not quietly un-stale the real finding it was built to keep reporting. + const v = stalenessOf(fixture(), SERVICE_PIN) + assert.equal(v.freshnessState, 'stale') + assert.equal(v.policyTargetVersion, '0.4.45', 'every intervening release is on the 0.4 line') + assert.equal(v.latestVersion, '0.4.45') +}) diff --git a/ts/src/vendor-graph.ts b/ts/src/vendor-graph.ts index 04defd8..1b313b5 100644 --- a/ts/src/vendor-graph.ts +++ b/ts/src/vendor-graph.ts @@ -734,6 +734,16 @@ export interface StalenessVerdict { /** The artifacts crossed, in release order. `releaseDistance === intervening.length`. */ intervening: InterveningArtifact[] freshnessPolicy: FreshnessPolicy + /** + * The newest release the POLICY is willing to take — which is not always `latestVersion`. + * + * `track-latest` takes the newest release; `pin-exact` takes nothing, so it is the pinned + * version; `track-minor` takes the newest release **on the pinned `x.y` line**, which is + * `0.4.47` even when `0.5.0` exists. This is the version `proposeRevendor` proposes: without + * it a policy that correctly REFUSES a cross-minor bump would still emit a proposal to make + * it, and the refusal would be a verdict nobody acts on. + */ + policyTargetVersion: string /** DERIVED, per the policy. */ freshnessState: FreshnessState /** DECLARED in the register. */ @@ -750,7 +760,23 @@ export interface StalenessVerdict { guardInvokedByCi?: boolean } -const majorOf = (v: string): string => v.split('.')[0] ?? v +/** + * The `x.y` line a version belongs to — the lane a `track-minor` pin follows — or `undefined` + * if the version is not release-shaped and therefore has no lane. + * + * Derived through `parseVersion`, not `split('.')`: `v0.4.46`, `0.4.46+build.7` and `0.4` all + * name a point on the `0.4` line, and a commit sha or the literal `unknown` names no line at + * all. A missing minor is `0` (`1` and `1.0` are the same lane), matching `compareVersions`, + * which already treats a missing component as `0`. + */ +function minorLineOf(v: string): string | undefined { + const p = parseVersion(v) + if (!p) return undefined + return `${p.release[0] ?? '0'}.${p.release[1] ?? '0'}` +} + +/** Is `v` a prerelease (`1.0.0-rc.1`)? Non-release-shaped strings are not. */ +const isPrerelease = (v: string): boolean => parseVersion(v)?.prerelease !== undefined /** * Derive the staleness of one pin from the graph. @@ -758,13 +784,41 @@ const majorOf = (v: string): string => v.split('.')[0] ?? v * PURE — reads only. Returns the release distance and the intervening artifacts, because the * actionable question is never "is it stale" but "how far, across what". * - * Policy semantics are the register's, implemented verbatim: + * Policy semantics are the register's: * • `pin-exact` — never stale, but ONLY if upstream has actually been OBSERVED. A pin to * something nobody has looked at is `unknown`, not `current` — it is an * unknown wearing a pin's clothes. - * • `track-minor` — stale if a newer artifact shares the pinned major, or if the newest - * artifact's major differs at all. + * • `track-minor` — the pin follows ONE MINOR LINE, `x.y.*`. Stale when a newer release lands + * ON that line; NOT stale when the only newer releases have left it. * • `track-latest` — stale if the distance is greater than zero. + * + * ── `track-minor` used to mean `track-latest` ─────────────────────────────────────────── + * The previous implementation was + * + * const sameMajorNewer = intervening.some((a) => majorOf(a.version) === majorOf(pinnedVersion)) + * const majorMoved = releaseDistance > 0 && majorOf(latestVersion) !== majorOf(pinnedVersion) + * freshnessState = sameMajorNewer || majorMoved ? 'stale' : 'current' + * + * — MAJOR, not minor, and the two arms between them are exhaustive: if nothing newer shares the + * pinned major then the newest cannot either, so `majorMoved` fires. `releaseDistance > 0` was + * therefore ALWAYS stale, and `track-minor` was `track-latest` with a major-shaped rationale + * string attached. Three declared policies, two distinct behaviours. A register author writing + * `freshness_policy: track-minor` — the DEFAULT, and what every pin in the live vendor register + * declares — was silently opted in to cross-minor bumps: a pin at `0.4.46` was reported stale + * against `0.5.0` and a re-vendor proposal to `0.5.0` was emitted and sealed for it. + * + * Now: a `track-minor` pin at `0.4.46` is stale against `0.4.47` and CURRENT against `0.5.0`. + * Leaving the line is a deliberate act — an owner editing the pin, or declaring `track-latest` — + * not something the freshness plane proposes on the owner's behalf. The rationale names the + * off-line releases anyway, so `current` here never means "nothing happened upstream". + * + * Two boundary rules, both stated so they are not rediscovered as bugs: + * · A pin whose version is NOT release-shaped (a sha, `unknown`) has no minor line to follow, + * so its state is `unknown`, not a guess in either direction — the same answer this function + * already gives an unobserved pin, for the same reason. + * · A PRERELEASE does not make a stable pin stale (`0.4.47-rc.1` leaves `0.4.46` current), + * matching how semver ranges treat prereleases: opt-in, never inherited. A pin that is + * itself a prerelease does follow them. */ export function stalenessOf(store: HellGraphStore, pinId: string): StalenessVerdict { const pin = store.getNode(pinId) @@ -789,6 +843,8 @@ export function stalenessOf(store: HellGraphStore, pinId: string): StalenessVerd const observed = releaseDistance > 0 || store.in(pinnedArtifact.id, VFP_EDGE.supersededBy).length > 0 let freshnessState: FreshnessState let rationale: string + // Defaults to the pinned version: a policy that takes nothing proposes nothing. + let policyTargetVersion = pinnedVersion if (!observed) { freshnessState = 'unknown' rationale = `no upstream release history for ${pinnedVersion} in the graph — unobserved, not current` @@ -797,18 +853,40 @@ export function stalenessOf(store: HellGraphStore, pinId: string): StalenessVerd rationale = `pin-exact against an observed upstream: ${releaseDistance} newer release(s) exist and are deliberately not taken` } else if (freshnessPolicy === 'track-latest') { freshnessState = releaseDistance > 0 ? 'stale' : 'current' + policyTargetVersion = latestVersion rationale = releaseDistance > 0 ? `track-latest: ${releaseDistance} release(s) behind ${latestVersion}` : `track-latest: pinned at the newest release ${pinnedVersion}` } else { - const sameMajorNewer = intervening.some((a) => majorOf(a.version) === majorOf(pinnedVersion)) - const majorMoved = releaseDistance > 0 && majorOf(latestVersion) !== majorOf(pinnedVersion) - freshnessState = sameMajorNewer || majorMoved ? 'stale' : 'current' - rationale = freshnessState === 'stale' - ? (sameMajorNewer - ? `track-minor: ${releaseDistance} newer release(s) share major ${majorOf(pinnedVersion)}, newest ${latestVersion}` - : `track-minor: newest release ${latestVersion} changes major from ${majorOf(pinnedVersion)}`) - : `track-minor: nothing newer within major ${majorOf(pinnedVersion)}` + const line = minorLineOf(pinnedVersion) + if (line === undefined) { + // No lane can be derived, so neither `stale` nor `current` is a fact. Saying either would + // be inventing one; `unknown` is the state this function already has for exactly that. + freshnessState = 'unknown' + rationale = `track-minor: pinned version '${pinnedVersion}' is not release-shaped, so it names no ` + + `x.y line to follow — undecidable, not current (declare pin-exact or track-latest, or pin a release)` + } else { + // In-lane: same x.y line, strictly newer by PRECEDENCE (not by chain position — a register + // may declare its releases out of order), and not a prerelease unless the pin is one. + const followsPre = isPrerelease(pinnedVersion) + const onLine = intervening.filter((a) => + minorLineOf(a.version) === line && + compareVersions(a.version, pinnedVersion) > 0 && + (followsPre || !isPrerelease(a.version))) + const offLine = intervening.filter((a) => minorLineOf(a.version) !== line) + freshnessState = onLine.length > 0 ? 'stale' : 'current' + const newestOnLine = onLine[onLine.length - 1]?.version + // The proposal target is the newest IN-LANE release, never `latestVersion`. `0.5.0` being + // newer is not a reason to propose it to a pin that follows `0.4.*`. + if (newestOnLine !== undefined) policyTargetVersion = newestOnLine + const offLineNote = offLine.length > 0 + ? ` (${offLine.length} newer release(s) have left the ${line} line — ${offLine.map((a) => a.version).join(', ')} — ` + + `not followed under track-minor)` + : '' + rationale = freshnessState === 'stale' + ? `track-minor: ${onLine.length} newer release(s) on the pinned ${line} line, newest ${newestOnLine}${offLineNote}` + : `track-minor: nothing newer on the pinned ${line} line${offLineNote || ` (newest overall ${latestVersion})`}` + } } // Agreement, not enforcement of freshness. A `current` declaration over a `stale` derivation is @@ -829,6 +907,7 @@ export function stalenessOf(store: HellGraphStore, pinId: string): StalenessVerd releaseDistance, intervening, freshnessPolicy, + policyTargetVersion, freshnessState, disposition, dispositionAgrees, @@ -1074,7 +1153,11 @@ export function proposeRevendor(store: HellGraphStore, pinId: string, opts: Prop const risk = contractCrossingRiskOf(store, pinId) const pin = store.getNode(pinId)! - const toVersion = str(pin.properties['targetVersion']) ?? verdict.latestVersion + // `policyTargetVersion`, not `latestVersion`. They differ exactly when the policy declines the + // newest release — a `track-minor` pin at 0.4.46 with 0.4.47 and 0.5.0 upstream is stale, and + // the proposal it earns is `→ 0.4.47`. Proposing `→ 0.5.0` there would hand a human the very + // cross-minor bump the policy exists to withhold, over a rationale that says it was withheld. + const toVersion = str(pin.properties['targetVersion']) ?? verdict.policyTargetVersion const location = verdict.artifactPath !== undefined ? `${verdict.consumerRepo}/${verdict.artifactPath}` : undefined From 565cdd6c3cf5b31e2f952822f00a7d4fd85ff494 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:28:45 -0400 Subject: [PATCH 2/3] fix(vendor-graph): track-minor target is the newest in-lane by PRECEDENCE, not the chain tail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `stalenessOf`'s track-minor arm set `policyTargetVersion` to `onLine[onLine.length - 1]` — the last on-line release in supersession-chain order. That chain is the register's DECLARED order, which the filter directly above treats as possibly out of order (it ranks membership with `compareVersions` for exactly that reason). Taking the chain tail as "newest" reintroduced the chain-vs-precedence defect this stack exists to remove, one level down: an out-of-order 0.4 line `[0.4.46, 0.4.9]` proposed `-> 0.4.9`, sealed a re-vendor receipt for it, and printed a rationale calling 0.4.9 the "newest". Reduce by `compareVersions` instead — the same ranking `newestReleasedVersion` already uses. The regression test drives the real ingest with releases declared out of precedence order; it is RED against the old `onLine[last]` (`'0.4.9' !== '0.4.46'`) and green now. Full suite 427/427, typecheck clean, ts/dist rebuilt. Addresses the Copilot review comment on pulls/44 ("newestOnLine is derived from the last element of onLine ... compute the max by compareVersions instead of relying on array order"). `policyTargetVersion` kept required — it is a derived output field, always produced — with the rationale documented on the interface. Follow-up, not fixed here: `latestVersion = intervening[intervening.length - 1]` makes the same declared-order assumption for track-latest; it is pre-existing and outside this diff. --- ts/dist/index.d.mts | Bin 229247 -> 229923 bytes ts/dist/index.d.ts | Bin 229247 -> 229923 bytes ts/dist/index.js | Bin 428271 -> 428348 bytes ts/dist/index.mjs | Bin 419411 -> 419488 bytes ts/src/vendor-graph.test.ts | 20 ++++++++++++++++++++ ts/src/vendor-graph.ts | 18 +++++++++++++++++- 6 files changed, 37 insertions(+), 1 deletion(-) diff --git a/ts/dist/index.d.mts b/ts/dist/index.d.mts index f9a9e650f1b06adb91a0d90973a354dd9ac98ff2..9b750ccaec4234cd0651b2b89839cb4602c837c0 100644 GIT binary patch delta 695 zcmY+CO>Wdc5QPQVA;B3QJBTK809YX(9y2x6l6o&C>Kn delta 21 dcmZ47!uS6lZ$k@X3)2?n-@4o98!)#70sw0=2?ziH diff --git a/ts/dist/index.d.ts b/ts/dist/index.d.ts index f9a9e650f1b06adb91a0d90973a354dd9ac98ff2..9b750ccaec4234cd0651b2b89839cb4602c837c0 100644 GIT binary patch delta 695 zcmY+CO>Wdc5QPQVA;B3QJBTK809YX(9y2x6l6o&C>Kn delta 21 dcmZ47!uS6lZ$k@X3)2?n-@4o98!)#70sw0=2?ziH diff --git a/ts/dist/index.js b/ts/dist/index.js index 98da9f84838da2f4e48b98e111c71341e82be2a2..bde3e69f03b861d2f3a4ffe8c7f4f2d1254a076f 100644 GIT binary patch delta 155 zcmaDqQEJa5sfHHD7N!>F7M2#)Eo|k6^?Es}dFdq?3U&$x3ib;5c|MtWsd`1JDW%D& z8XCDe3W=Huwss21`MCv&MX6z_Ma7xF7M2#)Eo|k6GST^YKACx`dO4|i=_MHox(bG|_IhQhMa7x< MdD|Ztvc-f006$6;0{{R3 diff --git a/ts/dist/index.mjs b/ts/dist/index.mjs index 22fb2cb1a4182d52934cede7333e7c9d80c0c5b7..be5e3a029ff231ce05b3d9473e8e76dc724a3f3d 100644 GIT binary patch delta 102 zcmcb7MsmSf$%Yoj7N!>F7M2#)Eo|p?rg!VG@rmc>`DEs$>J_D?lqRQYXyocBBx)+y z+D+fD%_gIft5=pS74V4z^Hkf>m#kgEw31#-&rGgA}{w!hY4%di9h>qH?T delta 56 zcmZ2*R`T*0$%Yoj7N!>F7M2#)Eo|p?WTNx)d@}P=^>R}4(n~TFbQKI^?e)r1i;6Sz M^R`>)vSnBT08~U1)Bpeg diff --git a/ts/src/vendor-graph.test.ts b/ts/src/vendor-graph.test.ts index 7484e8c..513d07f 100644 --- a/ts/src/vendor-graph.test.ts +++ b/ts/src/vendor-graph.test.ts @@ -975,6 +975,26 @@ test('a stale track-minor pin proposes the newest IN-LANE release, not the newes assert.deepEqual(p.contractViolations, []) }) +test('the in-lane target is the newest by PRECEDENCE, not the last release in the declared chain', () => { + // The filter above ranks with `compareVersions` precisely because "a register may declare its + // releases out of order" — the supersession chain is declared order, not precedence order. The + // proposal target has to honour the same rule. Picking `onLine[last]` (the chain tail) here would + // reintroduce the exact chain-vs-precedence defect this stack removes, one level down: an + // out-of-order 0.4 line `[0.4.46, 0.4.9]` would propose `→ 0.4.9`, seal a re-vendor receipt for + // it, and print a rationale calling 0.4.9 the "newest". 0.4.46 is newer and also on the line. + const store = minorLineFixture(['0.4.5', '0.4.46', '0.4.9'], '0.4.5') + const v = stalenessOf(store, LINE_PIN) + assert.equal(v.freshnessState, 'stale') + assert.equal(v.policyTargetVersion, '0.4.46', + 'newest IN-LANE by version, not 0.4.9 (the last release in the declared chain)') + assert.ok(v.rationale.includes('newest 0.4.46'), `rationale must name the real newest: ${v.rationale}`) + assert.ok(!v.rationale.includes('newest 0.4.9'), `rationale must not call 0.4.9 newest: ${v.rationale}`) + + const p = proposeRevendor(store, LINE_PIN, { requestedAt: '2026-07-30T00:00:00Z' }) + assert.equal(p.effectRequest.parameters['toVersion'], '0.4.46', + 'the sealed proposal targets the real newest in-lane release') +}) + test('track-latest still proposes the newest release — the split is per-policy, not global', () => { const store = minorLineFixture(['0.4.46', '0.4.47', '0.5.0'], '0.4.46', 'track-latest') const v = stalenessOf(store, LINE_PIN) diff --git a/ts/src/vendor-graph.ts b/ts/src/vendor-graph.ts index 1b313b5..a11622a 100644 --- a/ts/src/vendor-graph.ts +++ b/ts/src/vendor-graph.ts @@ -742,6 +742,14 @@ export interface StalenessVerdict { * `0.4.47` even when `0.5.0` exists. This is the version `proposeRevendor` proposes: without * it a policy that correctly REFUSES a cross-minor bump would still emit a proposal to make * it, and the refusal would be a verdict nobody acts on. + * + * Required, not optional. Like every other field on this verdict it is DERIVED and always + * produced — `stalenessOf` seeds it with `pinnedVersion` and only ever narrows it — and + * `StalenessVerdict` is an OUTPUT type: produced here, read by consumers, not constructed by + * them. Adding a field a reader will simply receive is not a break; making it optional would + * instead push an `undefined` that never occurs onto every read site (`proposeRevendor` reads it + * unconditionally). If a downstream ever does construct this verdict, that call site wanting the + * new field set is the correct signal, not a reason to weaken the type. */ policyTargetVersion: string /** DERIVED, per the policy. */ @@ -875,7 +883,15 @@ export function stalenessOf(store: HellGraphStore, pinId: string): StalenessVerd (followsPre || !isPrerelease(a.version))) const offLine = intervening.filter((a) => minorLineOf(a.version) !== line) freshnessState = onLine.length > 0 ? 'stale' : 'current' - const newestOnLine = onLine[onLine.length - 1]?.version + // The newest in-lane release by PRECEDENCE, not by chain position. `onLine` preserves the + // supersession-chain order, which is the register's declared order and — as the filter above + // says — may be out of order; `onLine[last]` would therefore reintroduce the exact chain-vs- + // precedence defect this stack removes, one level down (an out-of-order `[0.4.46, 0.4.9]` + // would propose `0.4.9` and seal a receipt whose rationale calls it "newest"). Reduce by + // `compareVersions` — the same ranking `newestReleasedVersion` uses for the same reason. + const newestOnLine = onLine.length > 0 + ? onLine.reduce((m, a) => (compareVersions(a.version, m.version) > 0 ? a : m)).version + : undefined // The proposal target is the newest IN-LANE release, never `latestVersion`. `0.5.0` being // newer is not a reason to propose it to a pin that follows `0.4.*`. if (newestOnLine !== undefined) policyTargetVersion = newestOnLine From a96b30762076eb551c3693577bbc8a5e0a327bbf Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:25:05 -0400 Subject: [PATCH 3/3] build: regenerate dist after rebase onto main --- ts/dist/index.d.mts | Bin 229923 -> 235834 bytes ts/dist/index.d.ts | Bin 229923 -> 235834 bytes ts/dist/index.js | Bin 428348 -> 432185 bytes ts/dist/index.mjs | Bin 419488 -> 423191 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/ts/dist/index.d.mts b/ts/dist/index.d.mts index 9b750ccaec4234cd0651b2b89839cb4602c837c0..92e64c8dc5930acb9abccb42392ef15337f89b23 100644 GIT binary patch delta 5986 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 delta 223 zcmWN}txg0n6hPs$4Sy?EfB<bW$X^6o$ z2VlU)K*?BwguZcM+yyWpQi%!lD5lDghN{j4wnTdYj(V^Qcs=pXI`Rz|f)0Z<-a~eT zu$>V>UtuU>8jO>flzWtQH!VOXyzJGO35z4rJJZdV^|m?}%e*p^hciCCQOB$N{-_3m S|DIGKcr;bdiZ@!dCI10Cyio@L diff --git a/ts/dist/index.d.ts b/ts/dist/index.d.ts index 9b750ccaec4234cd0651b2b89839cb4602c837c0..92e64c8dc5930acb9abccb42392ef15337f89b23 100644 GIT binary patch delta 5986 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 delta 223 zcmWN}txg0n6hPs$4Sy?EfB<bW$X^6o$ z2VlU)K*?BwguZcM+yyWpQi%!lD5lDghN{j4wnTdYj(V^Qcs=pXI`Rz|f)0Z<-a~eT zu$>V>UtuU>8jO>flzWtQH!VOXyzJGO35z4rJJZdV^|m?}%e*p^hciCCQOB$N{-_3m S|DIGKcr;bdiZ@!dCI10Cyio@L diff --git a/ts/dist/index.js b/ts/dist/index.js index bde3e69f03b861d2f3a4ffe8c7f4f2d1254a076f..604c6f3b8e13340f66f4ca8b1a3237c57c3d686e 100644 GIT binary patch delta 5009 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 delta 1486 zcmZuxaZDRk7|*-*4pu0UQ3_#mz7>VLw7r%wNk%0=+)NnEjg4-`QrNY9(39RByLZLS zu5p?XGP5CD6JCrXQU6j22?poO;zUrRQDfAYxW9D%VHOi(BF1I<*L>I3$cXpH%e(LU zz3+R!-}im*`f}5aD^1Jo@6TTa&O+}8x4B$?WzXt0&YN>V_9YD+ThKI~gkwI93IalFSpsvug-oo$<7n_R1uEN3fW^(E&Q0HQn2y^762pRN%hLzd% z?G92i2P&F#P?qI{s;5+aJOE=BGBsIOG!*ZIX+=+E5X`UK`D6#T^3i9BP|apEbL_jj z&%MP)XPhA7C>c)&?BHi$o(w(?c9M+>aD?%qhbzG{us3cOI0C$4F(@`NEWLdZnbC@O zl2@z1ZU?(D{k{s+g9rWtv5*?fzvl*51@hyfLm+R|fn80paYoo?j$`3?w`~7*cSAj! z$&jDwz$2A0MOH9_kXjG$G}vN5Sf-8T$t+HbM%JSJ9ZL&?2`yU?=@M%*3lzwgO1B9fWHWTg~)xJ7^9UlrKHx^Cb9zE~UV2 zo@|VO2MbGIH9PI>MdxqN03XNd^ZN*BC;by(;r}r%GLQk+Nwo&18?Cg;1XD~!!?EIC zN2e`Oijx%$1n>766Ee=INhG#OVS8-GXqc|utUIM6>a2M*xYj1NA za``Rpv7&5b@MF$1)3(UbwnfL29rU}O9jmS6^=fC!-eZ=@hDb!FDM_-4CNm179*PZ{ z!}_ol_lFn(AM{COO~L{AXgEy2sCI_Bw;@?W7%SswCOF`v!2!NiCLQPawWKGJpGL1APRz~_5qpmLK%bKbWwzR0*rbIq}7UHNu5rZQY;j- zR1HBD+hxXT3PBr-(~5~wa7xLj48vLjXA^96A$#6zDAHMkBm<<8$Z%HqaVP;OpuB7ll7LIx#KGJE(X;7~e*3ToLm8Kc0m3Bme*a diff --git a/ts/dist/index.mjs b/ts/dist/index.mjs index be5e3a029ff231ce05b3d9473e8e76dc724a3f3d..135f864dc58f9137c33960e3c90651b365a748c1 100644 GIT binary patch delta 4785 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 B6^sA? delta 1473 zcmZuxZ){Ul6wi6D@2xU{jg~na>ZOeBJ=(sODH2!5Sab<9Amg7z+p@j3x9uB!ujSp> z36_^Qjq)d`m?mC75Hn&F4G94ja-9*`65|J=iJGWY*v1vOL@p2zVE}fqiSJS)?M#Y+&rc*x&0e0O7r-2h?FUT+o9ZDlJDVKN+UNsu0 zm+q%+epn;&%IWhh5M*P10BHLc$+MftW!1ajw9He{KpTWsCkvds*9DS{rw29fb8rw@ z)l!Mm-!d!*>47#_{$NtqbmAeiWwTOs@KB+k=#fSo?~~W|g{Ba#QYA&DTy@x78_Wln zE*^?WH+1+A-T#%?LT`2YWP0JEPg=KVG^R$dqfAB+@rJTcNaGLc=IrVgvvGCV<#q5a zu(@7%SZKGjie0W?+c7Xn9>9mwOjr+F#CGRD_9vrI5!&@T?etm<;$4|L&8Qz`n#|}K z3M=U=L8!A2;&3olzwtwmZXSp4SV)H-B?>#?K3Xe6Z_}{@me5iGItApuu@mm6=L&G? z|1k4Kn59z&ywvFw4c<20)=iRZ+%0^_QPeb5CLp@%HgEZWMoLB&D;v}_J2U}rc6g%b znu0`2XHyiOEDif9PYS4{bR6Cq;uybAKi8mmNrG~;Ye1uIFcldwDwO-kqdCF z2kv3cNqA1CxKDauZN6+HrMXu%t^lq&8|RUW%6Y^Gy$k1wN<(!B`^!o0G*5Io9n?9^HD`bPHA4e?wrC^R-J z#8p+Zaiyelvaw{!PFX1@9ggv`Lnx#!bRix=8{=^{?e}#Lwqni1gy;qAj?-ITODm5q z-|Az_w@R0y^zE87*BaqE4RI=_?y$@e=}Uoyr=;(F^jcMVf{8Cn`^31BM+(Unv8g8) zkRi@yU;u@xwh=--J!p+VhBAEd_zvu~4f^^qsf&syq!lZ43+W3Ew0GCYxSC+$6H@5Y zeO%sXWcQA-$A|U|kL?-RKFqf$wm+4tJ>*9D*|%`+Y25o4ok~he<-+Qq^30KBIGSGM zQM+Rax@}b66u2$1=3wF|^sP}@rP_;3zv8zwPw7d*y0(o98knIkL6D;)^jQy n`cqoTx_8R=2H5tT9QCtTOL7$07p9y8_E=e-7TIsM91{Kp)93Cb