@preventive/triage 1.0.0-alpha.48 → 1.0.0-alpha.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/activity.js +3 -3
- package/out/{chunk-PENGGKX7.js → chunk-JEAUAJVF.js} +46 -47
- package/out/{chunk-X6PJAK3T.js → chunk-PBSOL2SL.js} +1 -1
- package/out/{chunk-MOACFRJ4.js → chunk-ULIF7A7N.js} +1 -1
- package/out/compare.js +1 -1
- package/out/dialogs.js +1 -1
- package/out/index.html +2 -2
- package/out/scan.js +1 -1
- package/out/view.css +1 -1
- package/out/view.js +1 -1
- package/package.json +1 -1
- package/server-managed/agent-merges.ts +28 -14
- package/server-managed/http.ts +74 -43
- package/server-managed/pull-comment-reviews.ts +1 -1
- package/server-managed/pull-dependency-reviews.ts +15 -8
- package/server-managed/repository-dependency-reviews.ts +5 -4
- package/server-managed/repository-pulse.ts +22 -8
package/out/view.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import"./chunk-R3NZFXVR.js";import{a as e,b as t,e as n,f as r,g as i,i as a}from"./chunk-MOACFRJ4.js";import{$b as o,$c as s,Ab as c,Ac as l,Ba as u,Bb as d,Bc as f,Bd as p,C as m,Cc as h,Cd as g,D as ee,Dc as te,Dd as ne,Ec as re,Ed as ie,Ee as ae,Fc as _,Fd as oe,Fe as se,G as ce,Ge as le,H as ue,He as de,I as fe,Ib as pe,Ie as me,K as he,Kb as ge,Kd as _e,L as v,Ld as ve,Mb as ye,N as be,Nc as xe,Ob as Se,Oc as Ce,P as we,Pb as Te,Q as Ee,Qb as De,R as Oe,Rb as ke,S as Ae,Sb as je,T as y,Tb as Me,Tc as Ne,Ua as Pe,Ub as Fe,Uc as Ie,V as Le,Va as Re,Vb as ze,Vc as Be,Wa as Ve,Wc as He,Xa as Ue,Xb as b,Xc as We,Ya as Ge,Yb as Ke,Yc as qe,Zb as Je,Zc as Ye,_b as Xe,ac as Ze,ad as Qe,bc as $e,bd as et,be as tt,c as nt,cb as rt,cc as x,ce as it,d as S,db as at,dc as ot,de as st,e as ct,eb as C,ec as lt,f as ut,fc as dt,fe as ft,gb as pt,gc as w,ge as mt,gf as ht,h as gt,hb as T,hc as _t,he as vt,hf as yt,i as bt,ia as xt,ib as St,ic as Ct,ie as E,if as wt,ja as Tt,jb as Et,jc as Dt,je as Ot,jf as D,k as kt,ka as At,kc as jt,ke as Mt,kf as Nt,l as Pt,la as Ft,lb as It,lc as Lt,le as Rt,lf as zt,m as Bt,ma as Vt,mb as Ht,mc as Ut,me as O,na as Wt,nc as Gt,nd as Kt,ne as qt,o as Jt,oa as Yt,oc as Xt,oe as Zt,of as Qt,p as $t,pc as en,pe as tn,pf as nn,q as rn,qc as an,qe as on,qf as sn,r as cn,ra as ln,rc as un,rf as dn,s as fn,sc as k,sf as pn,t as mn,tc as A,te as hn,ue as gn,v as _n,vb as vn,ve as yn,w as j,wb as bn,x as M,xb as xn,xc as Sn,y as Cn,yb as wn,yc as Tn,zb as En,zc as Dn}from"./chunk-PENGGKX7.js";import"./chunk-JCIIBLKR.js";import"./chunk-VH4XMZIW.js";import{d as On}from"./chunk-O6M574JI.js";import{D as kn,L as An,N as jn,T as Mn,Y as Nn,_ as N,b as Pn,n as Fn,t as In,u as Ln,v as Rn,x as zn,y as Bn}from"./chunk-6LNQTKAV.js";import{f as Vn,g as Hn}from"./chunk-7VAWINQF.js";import"./chunk-NRPC74AM.js";import"./chunk-7BKKYVWR.js";import{f as P}from"./chunk-7C7DWL7I.js";import"./chunk-PNHBZGDD.js";import"./chunk-VFWMQ3LG.js";import{C as Un,D as Wn,J as Gn,K as Kn,M as qn,S as Jn,V as Yn,W as Xn,Z as Zn,_ as Qn,a as F,b as $n,d as I,e as L,ea as er,f as tr,ga as nr,ia as rr,j as ir,r as ar,s as or,u as sr}from"./chunk-C2AVODGZ.js";import{g as cr}from"./chunk-IO7JTZ6O.js";import"./chunk-WCPJDAYA.js";import"./chunk-IYPY47MC.js";import{a as lr,d as ur}from"./chunk-PWFUOLZK.js";import{a as dr,g as fr,h as pr,n as mr,o as hr,q as gr}from"./chunk-BC2UCLFJ.js";import{B as _r,I as vr,l as yr}from"./chunk-SXV2T5C4.js";import{d as br,i as xr,k as Sr,l as Cr,q as wr,w as Tr}from"./chunk-WFYLMF6Z.js";import"./chunk-C4Z6ZBXV.js";import{a as R,b as Er,c as z,g as B,i as Dr,j as V}from"./chunk-2XUEZHIP.js";import"./chunk-TUII34K2.js";import"./chunk-OOJM4CTU.js";var Or={attach:{title:`Synced triage on report attach`,intro:`have triage from your workspace that disagrees with your local view on`,trailingNote:`Other findings in this report whose chain value didn't conflict were applied automatically; this list only covers disagreements. Pick which side to keep.`,importedSideLabel:`Apply from chain`,applyButton:`Apply`},chain:{title:`Triage conflict from peer update`,intro:`were changed by a peer (or by another tab) in a way that disagrees with your local triage on`,trailingNote:`Other findings whose remote value didn't conflict were applied automatically; this list only covers disagreements. Pick which side to keep for each — a "keep current" pick re-publishes your local value to the chain on your next save.`,importedSideLabel:`Apply from chain`,applyButton:`Apply`}};function kr(){d(async(e,t,n)=>{let r=we(e);try{return await vn(e,r,Or[n]??Or.attach)}catch{let t=e.length;return alert(`${n===`attach`?`A report attach`:`A workspace sync`} had ${t} triage disagreement${t===1?``:`s`} with your local view. Kept your local values because another dialog is open. ${n===`attach`?`Detach and re-attach the report to pick again.`:`Your local values will re-publish to the chain on the next change.`}`),null}})}var Ar=class extends It{static properties={retry:{type:Boolean},_value:{state:!0}};constructor(){super(),this.retry=!1,this._value=``}_finish(e){this._value=``,super._finish(e)}_onCancel=()=>this._finish(null);_onInput=e=>{this._value=e.target.value};_onSubmit=()=>{let e=this._value??``;e&&this._finish(e)};_onKeydown=e=>{e.key===`Enter`&&(e.preventDefault(),this._onSubmit())};render(){let e=(this._value??``).length>0,t=this.retry?`That password didn't match. Try again, or cancel to leave the workspace unsynced for now.`:`This sync server requires a password before it accepts a new workspace. Enter it to create the workspace on the server.`;return z`<dialog @close="${this._onClose}"><header><h3>Sync server password</h3></header><p class="nwd-intro">${t}</p><input type="password" class="nwd-input" placeholder="Password" autocomplete="off" data-1p-ignore="true" data-lpignore="true" data-bwignore="true" data-form-type="other" name="dv-sync-auth" maxlength="4096" .value="${this._value}" @input="${this._onInput}" @keydown="${this._onKeydown}"><footer class="nwd-actions"><span class="nwd-spacer"></span> <button type="button" @click="${this._onCancel}">Cancel</button> <button type="button" class="primary" ?disabled="${!e}" @click="${this._onSubmit}">${this.retry?`Retry`:`Authenticate`}</button></footer></dialog>`}};customElements.define(`sync-auth-dialog`,Ar);function jr({retry:e=!1}={}){return Ht(`sync-auth-dialog`,{retry:!!e})}function Mr(){c(async({retry:e})=>{try{return await jr({retry:e})}catch(e){return console.warn(`Sync auth dialog failed to open:`,e),null}})}var Nr=`https://triage.space/`,Pr=[{hostname:`deepaudit.dev`,pathPrefix:`/`,supportsSync:!0},{hostname:`chalker.github.io`,pathPrefix:`/deepview/`,supportsSync:!1}];function Fr(){for(let e of Pr)if(location.hostname===e.hostname&&location.pathname.startsWith(e.pathPrefix))return e;return null}var Ir=[`deepview.triage`,`deepview.triage.pending`],Lr=`deepview.repoUrls`;function Rr(){try{for(let e of Ir)if(localStorage.getItem(e))return!0}catch{return!0}try{let e=localStorage.getItem(Lr);if(e){let t=JSON.parse(e);if(t&&typeof t==`object`&&Object.keys(t).length>0)return!0}}catch{return!0}return!1}async function zr(){if(Rr())return!0;let e=[];try{e=await Cn()}catch{return!0}if(e.length>0)return!0;try{if(v().length>0)return!0}catch{return!0}try{return await m()}catch{return!0}}var Br=class extends V{static properties={supportsSync:{attribute:!1}};createRenderRoot(){return this}constructor(){super(),this.supportsSync=!1}firstUpdated(){let e=this.querySelector(`dialog`);e&&e.showModal();let t=this.querySelector(`button.om-stay`);t&&t.focus()}_finish(e){if(this._settled)return;this._settled=!0;let t=this.querySelector(`dialog`);t&&t.close(),this.dispatchEvent(new CustomEvent(`resolve`,{detail:e}))}_onClose=()=>this._finish(!1);_onStay=()=>this._finish(!1);_onMigrate=()=>this._finish(!0);render(){let e=()=>z`<a class="om-link" href="${Nr}" target="_blank" rel="noopener noreferrer">${Nr}</a>`;return z`<dialog class="origin-migration-dialog" @close="${this._onClose}"><header class="om-head"><h3>This deepview build has moved</h3><p><code>${location.host}${location.pathname}</code> has moved to ${e()}.</p></header><section class="om-body"><p>Workspaces and reports don't migrate automatically — they live in this origin's browser storage (OPFS where available, localStorage as a fallback) and have to be re-imported on ${e()}.</p>${this.supportsSync?z`<p>For each workspace, use the <strong>Export workspace</strong> button in the sidebar to download a <code>.gz</code> bundle (workspace + its reports + its triage), then drop that file on ${e()}. Workspaces that had sync configured will pick their triage state back up from the sync server once you re-import them with the same key.</p><p>For triage attached to loose reports (or as a top-up after re-importing workspaces), call <code>DeepView.export()</code> in the browser console — that downloads just the triage entries plus saved repo URLs, no reports or workspaces.</p>`:z`<p>This build has no sync layer. For each workspace, use the <strong>Export workspace</strong> button in the sidebar to download a <code>.gz</code> bundle (workspace + its reports + its triage), then drop that file on ${e()}.</p><p>For triage attached to loose reports, call <code>DeepView.export()</code> in the browser console — that downloads just the triage entries plus saved repo URLs, no reports or workspaces. Reports themselves need to be re-dropped from their original JSON on the new origin.</p>`}</section><footer class="om-actions"><button type="button" class="om-stay" @click="${this._onStay}">Keep editing on ${location.host}</button> <button type="button" class="primary om-go" @click="${this._onMigrate}">Switch to triage.space</button></footer></dialog>`}};customElements.define(`origin-migration-dialog`,Br);function Vr(e){return new Promise(t=>{let n=document.createElement(`origin-migration-dialog`);n.supportsSync=e.supportsSync,n.addEventListener(`resolve`,e=>{n.remove(),t(!!e.detail)}),document.body.append(n)})}async function Hr(){let e=Fr();return e?await zr()?await Vr(e)?(location.assign(Nr),!0):!1:(location.replace(Nr),!0):!1}var H=null,Ur=!1,Wr=null;function Gr(e){if(!e)return`Could not unlock. Try again.`;switch(e.name??``){case`NotAllowedError`:case`AbortError`:return``;case`SecurityError`:return`The site identity does not match the one this passkey was registered for. If you opened DeepView through a different URL (different subdomain, different port), reload via the original URL.`;case`InvalidStateError`:return`The passkey state on this device is inconsistent. Try reloading the page or removing and re-creating the passkey.`;case`NotSupportedError`:return`This browser does not support the WebAuthn PRF extension this site needs. Try a recent Chrome, Safari, or Edge with a platform authenticator.`;case`NetworkError`:return`A network error prevented the passkey from completing. Check your connection and try again.`;case`UnknownError`:return`The authenticator returned an unknown error. Try again, or use a different passkey.`;default:{let t=e.message??String(e);return t.length>200?t.slice(0,197)+`…`:t}}}function Kr(){if(H)return H;H=document.createElement(`div`),H.id=`lock-overlay`,H.setAttribute(`role`,`dialog`),H.setAttribute(`aria-modal`,`true`),H.setAttribute(`aria-label`,`Unlock DeepView`),Dr(z`<div class="lock-overlay-card"><div class="lock-overlay-icon" aria-hidden="true"><svg viewBox="0 0 32 32" width="40" height="40" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="15" width="18" height="12" rx="1.5"/><path d="M11 15v-4a5 5 0 0 1 10 0v4"/></svg></div><h2 class="lock-overlay-title">DeepView is locked</h2><p class="lock-overlay-body">Your triage data and reports are encrypted with a passkey on this device. Unlock to continue.</p><p class="lock-overlay-error" hidden></p><button type="button" class="lock-overlay-button primary">Unlock with passkey</button><p class="lock-overlay-footer">Lost your passkey? <a href="#" class="lock-overlay-wipe">Wipe local data and start over</a></p></div>`,H);let e=H.querySelector(`button`),t=H.querySelector(`.lock-overlay-error`);return e.addEventListener(`click`,async()=>{if(!Ur&&qr()){Ur=!0,t.hidden=!0,t.textContent=``,e.disabled=!0,e.textContent=`Waiting…`,Wr=new AbortController;try{await bt({signal:Wr.signal})}catch(e){let n=Gr(e);t.textContent=n,t.hidden=!n}finally{Ur=!1,Wr=null,e.disabled=!1,e.textContent=`Unlock with passkey`}}}),H.querySelector(`.lock-overlay-wipe`).addEventListener(`click`,async e=>{if(e.preventDefault(),!Ur&&qr()&&confirm(`Wipe ALL local DeepView data and start over?
|
|
1
|
+
import"./chunk-R3NZFXVR.js";import{a as e,b as t,e as n,f as r,g as i,i as a}from"./chunk-ULIF7A7N.js";import{$b as o,$c as s,Ab as c,Ac as l,Ba as u,Bb as d,Bc as f,Bd as p,C as m,Cc as h,Cd as g,D as ee,Dc as te,Dd as ne,Ec as re,Ed as ie,Ee as ae,Fc as _,Fd as oe,Fe as se,G as ce,Ge as le,H as ue,He as de,I as fe,Ib as pe,Ie as me,K as he,Kb as ge,Kd as _e,L as v,Ld as ve,Mb as ye,N as be,Nc as xe,Ob as Se,Oc as Ce,P as we,Pb as Te,Q as Ee,Qb as De,R as Oe,Rb as ke,S as Ae,Sb as je,T as y,Tb as Me,Tc as Ne,Ua as Pe,Ub as Fe,Uc as Ie,V as Le,Va as Re,Vb as ze,Vc as Be,Wa as Ve,Wc as He,Xa as Ue,Xb as b,Xc as We,Ya as Ge,Yb as Ke,Yc as qe,Zb as Je,Zc as Ye,_b as Xe,ac as Ze,ad as Qe,bc as $e,bd as et,be as tt,c as nt,cb as rt,cc as x,ce as it,d as S,db as at,dc as ot,de as st,e as ct,eb as C,ec as lt,f as ut,fc as dt,fe as ft,gb as pt,gc as w,ge as mt,gf as ht,h as gt,hb as T,hc as _t,he as vt,hf as yt,i as bt,ia as xt,ib as St,ic as Ct,ie as E,if as wt,ja as Tt,jb as Et,jc as Dt,je as Ot,jf as D,k as kt,ka as At,kc as jt,ke as Mt,kf as Nt,l as Pt,la as Ft,lb as It,lc as Lt,le as Rt,lf as zt,m as Bt,ma as Vt,mb as Ht,mc as Ut,me as O,na as Wt,nc as Gt,nd as Kt,ne as qt,o as Jt,oa as Yt,oc as Xt,oe as Zt,of as Qt,p as $t,pc as en,pe as tn,pf as nn,q as rn,qc as an,qe as on,qf as sn,r as cn,ra as ln,rc as un,rf as dn,s as fn,sc as k,sf as pn,t as mn,tc as A,te as hn,ue as gn,v as _n,vb as vn,ve as yn,w as j,wb as bn,x as M,xb as xn,xc as Sn,y as Cn,yb as wn,yc as Tn,zb as En,zc as Dn}from"./chunk-JEAUAJVF.js";import"./chunk-JCIIBLKR.js";import"./chunk-VH4XMZIW.js";import{d as On}from"./chunk-O6M574JI.js";import{D as kn,L as An,N as jn,T as Mn,Y as Nn,_ as N,b as Pn,n as Fn,t as In,u as Ln,v as Rn,x as zn,y as Bn}from"./chunk-6LNQTKAV.js";import{f as Vn,g as Hn}from"./chunk-7VAWINQF.js";import"./chunk-NRPC74AM.js";import"./chunk-7BKKYVWR.js";import{f as P}from"./chunk-7C7DWL7I.js";import"./chunk-PNHBZGDD.js";import"./chunk-VFWMQ3LG.js";import{C as Un,D as Wn,J as Gn,K as Kn,M as qn,S as Jn,V as Yn,W as Xn,Z as Zn,_ as Qn,a as F,b as $n,d as I,e as L,ea as er,f as tr,ga as nr,ia as rr,j as ir,r as ar,s as or,u as sr}from"./chunk-C2AVODGZ.js";import{g as cr}from"./chunk-IO7JTZ6O.js";import"./chunk-WCPJDAYA.js";import"./chunk-IYPY47MC.js";import{a as lr,d as ur}from"./chunk-PWFUOLZK.js";import{a as dr,g as fr,h as pr,n as mr,o as hr,q as gr}from"./chunk-BC2UCLFJ.js";import{B as _r,I as vr,l as yr}from"./chunk-SXV2T5C4.js";import{d as br,i as xr,k as Sr,l as Cr,q as wr,w as Tr}from"./chunk-WFYLMF6Z.js";import"./chunk-C4Z6ZBXV.js";import{a as R,b as Er,c as z,g as B,i as Dr,j as V}from"./chunk-2XUEZHIP.js";import"./chunk-TUII34K2.js";import"./chunk-OOJM4CTU.js";var Or={attach:{title:`Synced triage on report attach`,intro:`have triage from your workspace that disagrees with your local view on`,trailingNote:`Other findings in this report whose chain value didn't conflict were applied automatically; this list only covers disagreements. Pick which side to keep.`,importedSideLabel:`Apply from chain`,applyButton:`Apply`},chain:{title:`Triage conflict from peer update`,intro:`were changed by a peer (or by another tab) in a way that disagrees with your local triage on`,trailingNote:`Other findings whose remote value didn't conflict were applied automatically; this list only covers disagreements. Pick which side to keep for each — a "keep current" pick re-publishes your local value to the chain on your next save.`,importedSideLabel:`Apply from chain`,applyButton:`Apply`}};function kr(){d(async(e,t,n)=>{let r=we(e);try{return await vn(e,r,Or[n]??Or.attach)}catch{let t=e.length;return alert(`${n===`attach`?`A report attach`:`A workspace sync`} had ${t} triage disagreement${t===1?``:`s`} with your local view. Kept your local values because another dialog is open. ${n===`attach`?`Detach and re-attach the report to pick again.`:`Your local values will re-publish to the chain on the next change.`}`),null}})}var Ar=class extends It{static properties={retry:{type:Boolean},_value:{state:!0}};constructor(){super(),this.retry=!1,this._value=``}_finish(e){this._value=``,super._finish(e)}_onCancel=()=>this._finish(null);_onInput=e=>{this._value=e.target.value};_onSubmit=()=>{let e=this._value??``;e&&this._finish(e)};_onKeydown=e=>{e.key===`Enter`&&(e.preventDefault(),this._onSubmit())};render(){let e=(this._value??``).length>0,t=this.retry?`That password didn't match. Try again, or cancel to leave the workspace unsynced for now.`:`This sync server requires a password before it accepts a new workspace. Enter it to create the workspace on the server.`;return z`<dialog @close="${this._onClose}"><header><h3>Sync server password</h3></header><p class="nwd-intro">${t}</p><input type="password" class="nwd-input" placeholder="Password" autocomplete="off" data-1p-ignore="true" data-lpignore="true" data-bwignore="true" data-form-type="other" name="dv-sync-auth" maxlength="4096" .value="${this._value}" @input="${this._onInput}" @keydown="${this._onKeydown}"><footer class="nwd-actions"><span class="nwd-spacer"></span> <button type="button" @click="${this._onCancel}">Cancel</button> <button type="button" class="primary" ?disabled="${!e}" @click="${this._onSubmit}">${this.retry?`Retry`:`Authenticate`}</button></footer></dialog>`}};customElements.define(`sync-auth-dialog`,Ar);function jr({retry:e=!1}={}){return Ht(`sync-auth-dialog`,{retry:!!e})}function Mr(){c(async({retry:e})=>{try{return await jr({retry:e})}catch(e){return console.warn(`Sync auth dialog failed to open:`,e),null}})}var Nr=`https://triage.space/`,Pr=[{hostname:`deepaudit.dev`,pathPrefix:`/`,supportsSync:!0},{hostname:`chalker.github.io`,pathPrefix:`/deepview/`,supportsSync:!1}];function Fr(){for(let e of Pr)if(location.hostname===e.hostname&&location.pathname.startsWith(e.pathPrefix))return e;return null}var Ir=[`deepview.triage`,`deepview.triage.pending`],Lr=`deepview.repoUrls`;function Rr(){try{for(let e of Ir)if(localStorage.getItem(e))return!0}catch{return!0}try{let e=localStorage.getItem(Lr);if(e){let t=JSON.parse(e);if(t&&typeof t==`object`&&Object.keys(t).length>0)return!0}}catch{return!0}return!1}async function zr(){if(Rr())return!0;let e=[];try{e=await Cn()}catch{return!0}if(e.length>0)return!0;try{if(v().length>0)return!0}catch{return!0}try{return await m()}catch{return!0}}var Br=class extends V{static properties={supportsSync:{attribute:!1}};createRenderRoot(){return this}constructor(){super(),this.supportsSync=!1}firstUpdated(){let e=this.querySelector(`dialog`);e&&e.showModal();let t=this.querySelector(`button.om-stay`);t&&t.focus()}_finish(e){if(this._settled)return;this._settled=!0;let t=this.querySelector(`dialog`);t&&t.close(),this.dispatchEvent(new CustomEvent(`resolve`,{detail:e}))}_onClose=()=>this._finish(!1);_onStay=()=>this._finish(!1);_onMigrate=()=>this._finish(!0);render(){let e=()=>z`<a class="om-link" href="${Nr}" target="_blank" rel="noopener noreferrer">${Nr}</a>`;return z`<dialog class="origin-migration-dialog" @close="${this._onClose}"><header class="om-head"><h3>This deepview build has moved</h3><p><code>${location.host}${location.pathname}</code> has moved to ${e()}.</p></header><section class="om-body"><p>Workspaces and reports don't migrate automatically — they live in this origin's browser storage (OPFS where available, localStorage as a fallback) and have to be re-imported on ${e()}.</p>${this.supportsSync?z`<p>For each workspace, use the <strong>Export workspace</strong> button in the sidebar to download a <code>.gz</code> bundle (workspace + its reports + its triage), then drop that file on ${e()}. Workspaces that had sync configured will pick their triage state back up from the sync server once you re-import them with the same key.</p><p>For triage attached to loose reports (or as a top-up after re-importing workspaces), call <code>DeepView.export()</code> in the browser console — that downloads just the triage entries plus saved repo URLs, no reports or workspaces.</p>`:z`<p>This build has no sync layer. For each workspace, use the <strong>Export workspace</strong> button in the sidebar to download a <code>.gz</code> bundle (workspace + its reports + its triage), then drop that file on ${e()}.</p><p>For triage attached to loose reports, call <code>DeepView.export()</code> in the browser console — that downloads just the triage entries plus saved repo URLs, no reports or workspaces. Reports themselves need to be re-dropped from their original JSON on the new origin.</p>`}</section><footer class="om-actions"><button type="button" class="om-stay" @click="${this._onStay}">Keep editing on ${location.host}</button> <button type="button" class="primary om-go" @click="${this._onMigrate}">Switch to triage.space</button></footer></dialog>`}};customElements.define(`origin-migration-dialog`,Br);function Vr(e){return new Promise(t=>{let n=document.createElement(`origin-migration-dialog`);n.supportsSync=e.supportsSync,n.addEventListener(`resolve`,e=>{n.remove(),t(!!e.detail)}),document.body.append(n)})}async function Hr(){let e=Fr();return e?await zr()?await Vr(e)?(location.assign(Nr),!0):!1:(location.replace(Nr),!0):!1}var H=null,Ur=!1,Wr=null;function Gr(e){if(!e)return`Could not unlock. Try again.`;switch(e.name??``){case`NotAllowedError`:case`AbortError`:return``;case`SecurityError`:return`The site identity does not match the one this passkey was registered for. If you opened DeepView through a different URL (different subdomain, different port), reload via the original URL.`;case`InvalidStateError`:return`The passkey state on this device is inconsistent. Try reloading the page or removing and re-creating the passkey.`;case`NotSupportedError`:return`This browser does not support the WebAuthn PRF extension this site needs. Try a recent Chrome, Safari, or Edge with a platform authenticator.`;case`NetworkError`:return`A network error prevented the passkey from completing. Check your connection and try again.`;case`UnknownError`:return`The authenticator returned an unknown error. Try again, or use a different passkey.`;default:{let t=e.message??String(e);return t.length>200?t.slice(0,197)+`…`:t}}}function Kr(){if(H)return H;H=document.createElement(`div`),H.id=`lock-overlay`,H.setAttribute(`role`,`dialog`),H.setAttribute(`aria-modal`,`true`),H.setAttribute(`aria-label`,`Unlock DeepView`),Dr(z`<div class="lock-overlay-card"><div class="lock-overlay-icon" aria-hidden="true"><svg viewBox="0 0 32 32" width="40" height="40" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="15" width="18" height="12" rx="1.5"/><path d="M11 15v-4a5 5 0 0 1 10 0v4"/></svg></div><h2 class="lock-overlay-title">DeepView is locked</h2><p class="lock-overlay-body">Your triage data and reports are encrypted with a passkey on this device. Unlock to continue.</p><p class="lock-overlay-error" hidden></p><button type="button" class="lock-overlay-button primary">Unlock with passkey</button><p class="lock-overlay-footer">Lost your passkey? <a href="#" class="lock-overlay-wipe">Wipe local data and start over</a></p></div>`,H);let e=H.querySelector(`button`),t=H.querySelector(`.lock-overlay-error`);return e.addEventListener(`click`,async()=>{if(!Ur&&qr()){Ur=!0,t.hidden=!0,t.textContent=``,e.disabled=!0,e.textContent=`Waiting…`,Wr=new AbortController;try{await bt({signal:Wr.signal})}catch(e){let n=Gr(e);t.textContent=n,t.hidden=!n}finally{Ur=!1,Wr=null,e.disabled=!1,e.textContent=`Unlock with passkey`}}}),H.querySelector(`.lock-overlay-wipe`).addEventListener(`click`,async e=>{if(e.preventDefault(),!Ur&&qr()&&confirm(`Wipe ALL local DeepView data and start over?
|
|
2
2
|
|
|
3
3
|
This deletes your encrypted triage, all reports, and the passkey registration on this device. The data CANNOT be recovered after this — there is no backup.
|
|
4
4
|
|
package/package.json
CHANGED
|
@@ -37,14 +37,18 @@ const timeOf = (value: unknown) => {
|
|
|
37
37
|
// rebased onto the branch by their last commit there, when merged, and
|
|
38
38
|
// their commits' subjects, the oldest first (agentCommits); when the newest
|
|
39
39
|
// read was last updated, those updated since read again (`through`, null
|
|
40
|
-
// till all are read once)
|
|
41
|
-
//
|
|
42
|
-
//
|
|
40
|
+
// till all are read once), and where those read reach back no further than
|
|
41
|
+
// a period needed, when the oldest of them was updated (`reach`), every
|
|
42
|
+
// pull request merged since read (one is updated when merged, or after);
|
|
43
|
+
// and a read not finished: where it goes on (`after`), when the newest it
|
|
44
|
+
// began with was updated (`from`), the oldest it reads to (`stop`), how many
|
|
45
|
+
// it has read (`read`) and when the oldest of them was updated (`to`). And
|
|
46
|
+
// of every pull
|
|
43
47
|
// request merged, an agent's or not, its merge commit and when (`pulls`),
|
|
44
48
|
// and one rebased so (`pullRebases`), for the commits pushed to the branch
|
|
45
49
|
// directly to be told (directCommits).
|
|
46
|
-
export interface AgentMerges { merges: [string, number][]; rebases: Rebase[]; through: number | null
|
|
47
|
-
next: { after: string; from: number; stop: number; read: number } | null; pulls?: [string, number][]; pullRebases?: Rebase[] }
|
|
50
|
+
export interface AgentMerges { merges: [string, number][]; rebases: Rebase[]; through: number | null; reach?: number
|
|
51
|
+
next: { after: string; from: number; stop: number; read: number; to?: number } | null; pulls?: [string, number][]; pullRebases?: Rebase[] }
|
|
48
52
|
// A pull request rebased onto the branch: its last commit there, when it was
|
|
49
53
|
// merged, its latest commits' subjects, the oldest first, and how many more
|
|
50
54
|
// it had than those read (`more`).
|
|
@@ -86,20 +90,24 @@ const agentPull = ({ headRefName, labels, author }: MergedNode) => agentBranch(h
|
|
|
86
90
|
// first (a pull request is updated when merged, or after), on from what was
|
|
87
91
|
// `kept`: till one updated before the year, or before the newest read before
|
|
88
92
|
// was, as many queries an answer as MERGED_QUERIES, a read not finished
|
|
89
|
-
// going on from where it was left by the next
|
|
90
|
-
//
|
|
93
|
+
// going on from where it was left by the next, or once those merged since
|
|
94
|
+
// `need` are all read; one read again whose label no longer names an agent,
|
|
95
|
+
// what it was read to bring forgotten.
|
|
91
96
|
export async function agentMerges(fullName: string, token: string, since: number, kept: AgentMerges | null,
|
|
92
|
-
fetchImpl: typeof fetch = globalThis.fetch): Promise<AgentMerges> {
|
|
97
|
+
fetchImpl: typeof fetch = globalThis.fetch, need = since): Promise<AgentMerges> {
|
|
93
98
|
const [owner, name] = fullName.split('/') as [string, string]
|
|
94
99
|
const merges = new Map((kept?.merges ?? []).filter(([, at]) => at >= since))
|
|
95
100
|
const rebases = new Map((kept?.rebases ?? []).filter(([, at]) => at >= since).map(([sha, at, subjects, more = 0]) => [sha, [at, subjects, more] as const]))
|
|
96
101
|
const pulls = new Map((kept?.pulls ?? []).filter(([, at]) => at >= since))
|
|
97
102
|
const pullRebases = new Map((kept?.pullRebases ?? []).filter(([, at]) => at >= since).map(([sha, at, subjects, more = 0]) => [sha, [at, subjects, more] as const]))
|
|
98
|
-
|
|
103
|
+
// Those read reaching back no further than they needed, and `need`
|
|
104
|
+
// further: read again from the newest on, as far back as it needs.
|
|
105
|
+
const further = !kept?.next && kept?.reach !== undefined && kept.reach >= need
|
|
106
|
+
const stop = kept?.next?.stop ?? (further ? since : Math.max(since, kept?.through ?? since))
|
|
99
107
|
const listed = (map: typeof rebases) => [...map].map(([sha, [at, subjects, more]]): Rebase => more > 0 ? [sha, at, subjects, more] : [sha, at, subjects])
|
|
100
|
-
const result = (through: number | null, next: AgentMerges['next']): AgentMerges => ({ merges: [...merges], rebases: listed(rebases), through,
|
|
101
|
-
pulls: [...pulls], pullRebases: listed(pullRebases) })
|
|
102
|
-
let { after = null, from = null, read = 0 }: { after?: string | null; from?: number | null; read?: number } = kept?.next ?? {}
|
|
108
|
+
const result = (through: number | null, next: AgentMerges['next'], reach?: number): AgentMerges => ({ merges: [...merges], rebases: listed(rebases), through,
|
|
109
|
+
...reach === undefined ? {} : { reach }, next, pulls: [...pulls], pullRebases: listed(pullRebases) })
|
|
110
|
+
let { after = null, from = null, read = 0, to = null }: { after?: string | null; from?: number | null; read?: number; to?: number | null } = kept?.next ?? {}
|
|
103
111
|
for (let queries = 0; queries < MERGED_QUERIES; queries++) {
|
|
104
112
|
const page = (await githubGraphql(MERGED_QUERY, { owner, name, after }, token, fetchImpl) as MergedPage | null)?.repository?.pullRequests
|
|
105
113
|
let done = page?.pageInfo?.hasNextPage !== true || typeof page.pageInfo.endCursor !== 'string'
|
|
@@ -108,6 +116,7 @@ export async function agentMerges(fullName: string, token: string, since: number
|
|
|
108
116
|
if (updated === null) continue
|
|
109
117
|
if (updated < stop) { done = true; break }
|
|
110
118
|
from ??= updated
|
|
119
|
+
to = updated
|
|
111
120
|
read++
|
|
112
121
|
const [merged, merge] = [timeOf(node?.mergedAt), node?.mergeCommit?.oid]
|
|
113
122
|
if (!node || merged === null || merged < since || typeof merge !== 'string' || !SHA.test(merge)) continue
|
|
@@ -131,10 +140,15 @@ export async function agentMerges(fullName: string, token: string, since: number
|
|
|
131
140
|
for (const oid of brought) merges.set(oid, merged)
|
|
132
141
|
if (subjects) rebases.set(merge, [merged, subjects, more])
|
|
133
142
|
}
|
|
134
|
-
|
|
143
|
+
// Read as far back as it reads to: the year's start, every one since
|
|
144
|
+
// read; else as far as those read before reached.
|
|
145
|
+
if (done) return result(from ?? stop, null, stop > since ? kept?.reach : undefined)
|
|
135
146
|
after = page!.pageInfo!.endCursor as string
|
|
147
|
+
// Read as far back as `need`: finished, so far back, those updated
|
|
148
|
+
// since read by the next as ever.
|
|
149
|
+
if (need > since && to !== null && to < need) return result(Math.max(from ?? stop, kept?.through ?? Number.NEGATIVE_INFINITY), null, to)
|
|
136
150
|
}
|
|
137
|
-
return result(kept?.through ?? null, { after: after!, from: from ?? stop, stop, read })
|
|
151
|
+
return result(kept?.through ?? null, { after: after!, from: from ?? stop, stop, read, ...to === null ? {} : { to } }, kept?.reach)
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
// Whether two reads of the pull requests merged (agentMerges) bring the
|
package/server-managed/http.ts
CHANGED
|
@@ -476,20 +476,26 @@ function firstHeader(v: string | string[] | undefined): string | null {
|
|
|
476
476
|
return null
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
+
// A user's avatar, drawn on every tile, comment and review by them: kept by
|
|
480
|
+
// the browser five minutes, then shown as kept for a day while the browser
|
|
481
|
+
// asks again behind it, its image named by its ETag, so that an unchanged one
|
|
482
|
+
// answers with none (304). One changed on another's profile shows within
|
|
483
|
+
// minutes; a user's own change, at once, as their page asks for it again
|
|
484
|
+
// (ui/view/github-users.js refreshAvatar).
|
|
485
|
+
const USER_AVATAR_CACHE = 'private, max-age=300, stale-while-revalidate=86400'
|
|
486
|
+
|
|
479
487
|
// `key` is the image chosen for `id`; one not cached falls back to `id`'s own.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
sendAvatar(res, await avatarStore.get(key) ?? (key === id ? null : await avatarStore.get(id)), cacheControl)
|
|
488
|
+
async function serveAvatar(req: IncomingMessage, res: ServerResponse, avatarStore: AvatarStore, id: string, key = id, cacheControl = USER_AVATAR_CACHE): Promise<void> {
|
|
489
|
+
sendAvatar(req, res, await avatarStore.get(key) ?? (key === id ? null : await avatarStore.get(id)), cacheControl)
|
|
483
490
|
}
|
|
484
491
|
|
|
485
|
-
function sendAvatar(res: ServerResponse, avatar: CachedAvatar | null, cacheControl
|
|
492
|
+
function sendAvatar(req: IncomingMessage, res: ServerResponse, avatar: CachedAvatar | null, cacheControl: string): void {
|
|
486
493
|
if (avatar == null) { sendJson(res, 404, { error: 'no-avatar' }); return }
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
})
|
|
494
|
+
const etag = `"${createHash('sha256').update(`${avatar.contentType}\0`).update(avatar.bytes).digest('base64url').slice(0, 22)}"`
|
|
495
|
+
const headers = { 'etag': etag, 'cache-control': cacheControl, 'x-content-type-options': 'nosniff' }
|
|
496
|
+
const asked = req.headers['if-none-match']
|
|
497
|
+
if (typeof asked === 'string' && asked.split(',').some(tag => ['*', etag, `W/${etag}`].includes(tag.trim()))) { res.writeHead(304, headers); res.end(); return }
|
|
498
|
+
res.writeHead(200, { ...headers, 'content-type': avatar.contentType, 'content-length': String(avatar.bytes.length) })
|
|
493
499
|
res.end(avatar.bytes)
|
|
494
500
|
}
|
|
495
501
|
|
|
@@ -505,15 +511,15 @@ const AGENT_AVATAR = /^(app|user)-([1-9]\d{0,11})$/u
|
|
|
505
511
|
// for it alone.
|
|
506
512
|
const AGENT_AVATAR_CACHE = 'private, max-age=86400'
|
|
507
513
|
|
|
508
|
-
async function serveAgentAvatar(res: ServerResponse, avatarStore: AvatarStore, avatar: string): Promise<void> {
|
|
514
|
+
async function serveAgentAvatar(req: IncomingMessage, res: ServerResponse, avatarStore: AvatarStore, avatar: string): Promise<void> {
|
|
509
515
|
const [, kind, id] = AGENT_AVATAR.exec(avatar)!
|
|
510
516
|
const key = `00000000-0000-0000-000${kind === 'app' ? 0 : 1}-${id!.padStart(12, '0')}`
|
|
511
517
|
// Read once where it is kept; read again only past fetching it.
|
|
512
518
|
const kept = await avatarStore.get(key)
|
|
513
|
-
if (kept != null) { sendAvatar(res, kept, AGENT_AVATAR_CACHE); return }
|
|
519
|
+
if (kept != null) { sendAvatar(req, res, kept, AGENT_AVATAR_CACHE); return }
|
|
514
520
|
await cacheAvatar(avatarStore, key, `https://avatars.githubusercontent.com/${kind === 'app' ? 'in' : 'u'}/${id}`, globalThis.fetch)
|
|
515
521
|
.catch((err) => { console.warn('managed: agent avatar caching failed:', err) })
|
|
516
|
-
await serveAvatar(res, avatarStore, key, key, AGENT_AVATAR_CACHE)
|
|
522
|
+
await serveAvatar(req, res, avatarStore, key, key, AGENT_AVATAR_CACHE)
|
|
517
523
|
}
|
|
518
524
|
|
|
519
525
|
const MAX_JSON_BODY_BYTES = 4096
|
|
@@ -2639,7 +2645,7 @@ async function handleRepositoryPulse(res: ServerResponse, deps: ManagedHttpDeps,
|
|
|
2639
2645
|
await answerWholeRepository(res, deps, cookie, repoId, async (fullName, token) => {
|
|
2640
2646
|
const [period, year] = await Promise.all([
|
|
2641
2647
|
keptAnswer(deps, `${repoId}\0pulse\0${weeks}`, () => repositoryActivity(fullName, token, weeks)),
|
|
2642
|
-
pulseCommits(deps, repoId, fullName, token),
|
|
2648
|
+
pulseCommits(deps, repoId, fullName, token, weeks),
|
|
2643
2649
|
])
|
|
2644
2650
|
return { value: { weeks, ...period.value, stats: year.stats && periodStats(year.stats, weeks), reading: year.reading, pulls: year.pulls,
|
|
2645
2651
|
pending: year.pending }, kept: period.kept && year.kept }
|
|
@@ -2654,14 +2660,21 @@ async function handleRepositoryPulse(res: ServerResponse, deps: ManagedHttpDeps,
|
|
|
2654
2660
|
// read on by the next answer as the year's commits are, and read again as
|
|
2655
2661
|
// an answer kept is, a label changing whose a pull request is without
|
|
2656
2662
|
// moving the head, the year counted again where what they bring changed.
|
|
2657
|
-
//
|
|
2658
|
-
//
|
|
2659
|
-
//
|
|
2660
|
-
//
|
|
2663
|
+
// Read no further back than the period asked (`weeks`) needs, its weeks and
|
|
2664
|
+
// as many before them, the rest read only as a longer one asks, its count
|
|
2665
|
+
// so said (`from`). A year not read as far as it needs yet is how many of
|
|
2666
|
+
// its commits are (`reading`), or of the pull requests merged (`pulls`),
|
|
2667
|
+
// beside the year last counted from an earlier head, where one is kept over
|
|
2668
|
+
// the same weeks or a few weeks earlier, so that a head moved on, or the
|
|
2669
|
+
// rules its commits are read by, shows the year as it was till it is
|
|
2670
|
+
// counted again; one past
|
|
2661
2671
|
// YEAR_COMMITS is GitHub's statistics' (repositoryStats, repositoryLines),
|
|
2662
2672
|
// kept together as an issue's page is and through GitHub computing them
|
|
2663
2673
|
// again, `pending` while it first computes them, the year last counted
|
|
2664
2674
|
// beside it so too.
|
|
2675
|
+
// How many weeks behind the year shown a year last counted may be shown
|
|
2676
|
+
// while it is counted again: the year as it was, its latest weeks missing.
|
|
2677
|
+
const PULSE_LAST_MS = 28 * 24 * 60 * 60_000
|
|
2665
2678
|
// A day as a pulse's items are narrowed by (YYYY-MM-DD, UTC), when it starts;
|
|
2666
2679
|
// a GitHub login, as many of them as PULSE_AUTHORS.
|
|
2667
2680
|
const PULSE_DAY = /^\d{4}-\d{2}-\d{2}$/u
|
|
@@ -2693,8 +2706,13 @@ async function handleRepositoryPulseItems(res: ServerResponse, deps: ManagedHttp
|
|
|
2693
2706
|
})
|
|
2694
2707
|
}
|
|
2695
2708
|
|
|
2696
|
-
async function pulseCommits(deps: ManagedHttpDeps, repoId: number, fullName: string, token: string) {
|
|
2697
|
-
const
|
|
2709
|
+
async function pulseCommits(deps: ManagedHttpDeps, repoId: number, fullName: string, token: string, weeks = 52) {
|
|
2710
|
+
const now = Date.now()
|
|
2711
|
+
const first = pulseSince(52, now)
|
|
2712
|
+
// As far back as a period of `weeks` weeks needs the year read whole: its
|
|
2713
|
+
// weeks and as many before them, which it is shown beside; the year's,
|
|
2714
|
+
// all of it.
|
|
2715
|
+
const need = Math.max(first, pulseSince(2 * weeks, now))
|
|
2698
2716
|
const head = await keptAnswer(deps, `${repoId}\0pulse-head`, async () => ({ sha: await defaultHead(fullName, token) }))
|
|
2699
2717
|
if (!head.value.sha) return { stats: commitStats([], first), reading: null, pulls: null, pending: false, kept: head.kept }
|
|
2700
2718
|
const store = repositoryReadStore(deps.db, deps.config.debug)
|
|
@@ -2703,7 +2721,7 @@ async function pulseCommits(deps: ManagedHttpDeps, repoId: number, fullName: str
|
|
|
2703
2721
|
const mergesKey = `${repoId}\0pulse-merges\0${PULSE_YEAR_VERSION}`
|
|
2704
2722
|
const last = async () => {
|
|
2705
2723
|
const kept = (await store.read(lastKey))?.value as { first: number; stats: PulseStats } | undefined
|
|
2706
|
-
return kept
|
|
2724
|
+
return kept && first - kept.first <= PULSE_LAST_MS ? kept.stats : null
|
|
2707
2725
|
}
|
|
2708
2726
|
const sha = head.value.sha
|
|
2709
2727
|
// What tells whose the year's commits are, kept as read: the agents' pull
|
|
@@ -2711,47 +2729,60 @@ async function pulseCommits(deps: ManagedHttpDeps, repoId: number, fullName: str
|
|
|
2711
2729
|
// repository (`releases`, release-commits.ts releaseCommits).
|
|
2712
2730
|
type Attribution = AgentMerges & { releases?: string[] }
|
|
2713
2731
|
const keptAttribution = async () => ((await store.read(mergesKey))?.value as Attribution | undefined) ?? null
|
|
2714
|
-
const attribution = async (kept: Attribution | null): Promise<Attribution> => {
|
|
2715
|
-
const [merges, releases] = await Promise.all([agentMerges(fullName, token, first, kept
|
|
2732
|
+
const attribution = async (kept: Attribution | null, whole: number): Promise<Attribution> => {
|
|
2733
|
+
const [merges, releases] = await Promise.all([agentMerges(fullName, token, first, kept, globalThis.fetch, whole),
|
|
2734
|
+
releaseCommits(fullName, token, first, historyChanges(deps, repoId))])
|
|
2716
2735
|
return { ...merges, releases }
|
|
2717
2736
|
}
|
|
2737
|
+
// Since when every pull request merged is read (agentMerges `reach`, or
|
|
2738
|
+
// a read not finished's `to`): all of them, -∞; none yet, ∞.
|
|
2739
|
+
const readTo = (read: Attribution) => read.next ? read.next.to ?? Number.POSITIVE_INFINITY : read.reach ?? Number.NEGATIVE_INFINITY
|
|
2718
2740
|
// The year counted from its `commits` as `read` tells whose they are and
|
|
2719
2741
|
// which were pushed directly, the branch followed through those it was
|
|
2720
|
-
// read on past (`passed`, yearCommits) too
|
|
2721
|
-
//
|
|
2722
|
-
|
|
2742
|
+
// read on past (`passed`, yearCommits) too, whole from when both the
|
|
2743
|
+
// commits and the pull requests merged are read (`from`, none where all
|
|
2744
|
+
// are); kept by its head and as the year last counted.
|
|
2745
|
+
const count = async ({ commits, passed, from }: { commits: HistoryCommit[]; passed: HistoryCommit[]; from: number | null }, read: Attribution) => {
|
|
2723
2746
|
const walked = [...commits, ...passed]
|
|
2724
|
-
const
|
|
2747
|
+
const whole = Math.max(from ?? Number.NEGATIVE_INFINITY, readTo(read))
|
|
2748
|
+
const stats = { ...commitStats(commits, first, agentCommits(read, walked), new Set(read.releases), directCommits(read, walked, sha)),
|
|
2749
|
+
...Number.isFinite(whole) ? { from: whole } : {} }
|
|
2725
2750
|
await Promise.all([store.write(key, stats, Date.now()), store.write(lastKey, { first, stats }, Date.now())])
|
|
2726
2751
|
return stats
|
|
2727
2752
|
}
|
|
2728
2753
|
// A year kept by its head, what tells whose its commits are read again as
|
|
2729
2754
|
// an answer kept is (keptAnswer), a label or a tag changing it without
|
|
2730
|
-
// moving the head: counted again
|
|
2731
|
-
// pull requests bring, or the releases, changed,
|
|
2732
|
-
// requests
|
|
2733
|
-
// meanwhile, and where reading them fails.
|
|
2755
|
+
// moving the head: counted again, as far back as it was, from its pages
|
|
2756
|
+
// kept where what agents' pull requests bring, or the releases, changed,
|
|
2757
|
+
// or the pull requests merged are read further back; the year as kept
|
|
2758
|
+
// answered meanwhile, and where reading them fails.
|
|
2734
2759
|
const relabelled = async (stats: PulseStats) => {
|
|
2735
2760
|
const recounted: { stats?: PulseStats } = {}
|
|
2761
|
+
const whole = stats.from ?? first
|
|
2736
2762
|
await keptAnswer(deps, mergesKey, async () => {
|
|
2737
2763
|
const kept = await keptAttribution()
|
|
2738
|
-
const read = await attribution(kept)
|
|
2739
|
-
const same = kept &&
|
|
2740
|
-
if (read
|
|
2741
|
-
const year = await yearCommits(fullName, token, sha, first, historyPages(deps, repoId, true))
|
|
2764
|
+
const read = await attribution(kept, whole)
|
|
2765
|
+
const same = kept && sameMerges(read, kept) && read.releases!.join() === (kept.releases ?? []).join() && readTo(kept) <= readTo(read)
|
|
2766
|
+
if (readTo(read) >= whole || same) return read
|
|
2767
|
+
const year = await yearCommits(fullName, token, sha, first, historyPages(deps, repoId, true), globalThis.fetch, whole)
|
|
2742
2768
|
if (year.complete) recounted.stats = await count(year, read)
|
|
2743
2769
|
return read
|
|
2744
2770
|
}, () => true).catch((err: unknown) => { console.warn('managed: pulse attribution read failed:', err) })
|
|
2745
2771
|
return recounted.stats ?? stats
|
|
2746
2772
|
}
|
|
2773
|
+
// A year kept, read whole as far back as the period needs, as it was;
|
|
2774
|
+
// else its commits and the pull requests merged read on together as far
|
|
2775
|
+
// as it needs, and counted once both are.
|
|
2747
2776
|
const held = (await store.read(key))?.value as PulseStats | { tooMany: true } | undefined
|
|
2748
|
-
if (held && !('tooMany' in held)
|
|
2749
|
-
|
|
2750
|
-
|
|
2777
|
+
if (held && !('tooMany' in held) && (held.from === undefined || held.from < need)) {
|
|
2778
|
+
return { stats: await relabelled(held), reading: null, pulls: null, pending: false, kept: head.kept }
|
|
2779
|
+
}
|
|
2780
|
+
if (!held || !('tooMany' in held)) {
|
|
2781
|
+
const [year, read] = await Promise.all([yearCommits(fullName, token, sha, first, historyPages(deps, repoId, true), globalThis.fetch, need),
|
|
2782
|
+
keptAttribution().then(kept => attribution(kept, need))])
|
|
2783
|
+
await store.write(mergesKey, read, Date.now())
|
|
2751
2784
|
if (year.complete) {
|
|
2752
|
-
|
|
2753
|
-
await store.write(mergesKey, read, Date.now())
|
|
2754
|
-
if (read.next) return { stats: await last(), reading: null, pulls: read.next.read, pending: false, kept: false }
|
|
2785
|
+
if (readTo(read) >= need) return { stats: await last(), reading: null, pulls: read.next?.read ?? 0, pending: false, kept: false }
|
|
2755
2786
|
return { stats: await count(year, read), reading: null, pulls: null, pending: false, kept: false }
|
|
2756
2787
|
}
|
|
2757
2788
|
if (!year.tooMany) return { stats: await last(), reading: year.read, pulls: null, pending: false, kept: false }
|
|
@@ -4851,8 +4882,8 @@ export function createManagedRequestHandler(deps: ManagedHttpDeps): Handler {
|
|
|
4851
4882
|
if (path.startsWith(AVATAR_PREFIX)) {
|
|
4852
4883
|
if (method !== 'GET') { send405(res, 'GET'); return }
|
|
4853
4884
|
const id = path.slice(AVATAR_PREFIX.length)
|
|
4854
|
-
if (AGENT_AVATARS.has(id) && AGENT_AVATAR.test(id)) { await serveAgentAvatar(res, avatarStore, id); return }
|
|
4855
|
-
await serveAvatar(res, avatarStore, id, url.searchParams.has('own') ? id : await db.getAvatarKey(id))
|
|
4885
|
+
if (AGENT_AVATARS.has(id) && AGENT_AVATAR.test(id)) { await serveAgentAvatar(req, res, avatarStore, id); return }
|
|
4886
|
+
await serveAvatar(req, res, avatarStore, id, url.searchParams.has('own') ? id : await db.getAvatarKey(id))
|
|
4856
4887
|
return
|
|
4857
4888
|
}
|
|
4858
4889
|
// Admin: list users (admin-only).
|
|
@@ -132,7 +132,7 @@ function counted(answer: CommentReview[], read: Authored[], reviewers: Map<strin
|
|
|
132
132
|
const { name, version, integrity, patchHash, status } = dependency ?? {}
|
|
133
133
|
if (typeof name !== 'string' || typeof version !== 'string' || typeof integrity !== 'string' || !VERDICTS.has(status)) return []
|
|
134
134
|
return integrity.split(/\s+/u).filter(Boolean).map(hash => ({ package: name, version, integrity: hash, ...typeof patchHash === 'string' && patchHash ? { patchHash } : {},
|
|
135
|
-
basePackage: null, baseVersion: null, reviewer, status: status as ReviewVerdict, full: true, time }))
|
|
135
|
+
basePackage: null, baseVersion: null, reviewer, login: review.user as string, status: status as ReviewVerdict, full: true, time }))
|
|
136
136
|
})
|
|
137
137
|
})
|
|
138
138
|
}
|
|
@@ -107,7 +107,9 @@ const ACCEPTING = new Set(['accept', 'accept-with-nits'])
|
|
|
107
107
|
// review of theirs, whole or compared with one of `bases` of its package
|
|
108
108
|
// (any, for `any`); and how many review it in its pull request's comments
|
|
109
109
|
// alone (`commented`), which count for it alone: whose reviews passing it,
|
|
110
|
-
// else whose latest, are its comments'.
|
|
110
|
+
// else whose latest, are its comments'. Each reviewer too (`reviewers`), by
|
|
111
|
+
// their login, their latest verdict, whether they pass it, and whether that
|
|
112
|
+
// verdict is its comments' (`comment`), which counts for it alone.
|
|
111
113
|
function stancesOf(pkg: LockedPackage, reviews: ReviewsOf, bases: string[] | 'any') {
|
|
112
114
|
const hashes = hashesOf(pkg)
|
|
113
115
|
const by = new Map<string, PublishedReview[]>()
|
|
@@ -118,17 +120,21 @@ function stancesOf(pkg: LockedPackage, reviews: ReviewsOf, bases: string[] | 'an
|
|
|
118
120
|
const qualifies = (review: PublishedReview) => review.full && ACCEPTING.has(review.status)
|
|
119
121
|
&& (review.basePackage === null || (review.basePackage === pkg.name && (bases === 'any' || bases.includes(review.baseVersion ?? ''))))
|
|
120
122
|
let commented = 0, passing = 0, rejected = false
|
|
123
|
+
const reviewers: Reviewer[] = []
|
|
121
124
|
for (const theirs of by.values()) {
|
|
122
125
|
const latest = theirs.reduce((one, other) => other.time > one.time ? other : one)
|
|
126
|
+
const passes = ACCEPTING.has(latest.status) && theirs.some(qualifies)
|
|
123
127
|
// In its comments alone where those telling where they stand are: those
|
|
124
128
|
// passing it, where they pass it, else their latest.
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
129
|
+
const comment = (passes ? theirs.filter(qualifies) : [latest]).every(review => review.comment)
|
|
130
|
+
if (comment) commented++
|
|
127
131
|
if (latest.status === 'reject') rejected = true
|
|
128
|
-
else if (
|
|
132
|
+
else if (passes) passing++
|
|
133
|
+
reviewers.push({ login: latest.login, status: latest.status, ...passes ? { passes: true as const } : {}, ...latest.comment ? { comment: true as const } : {} })
|
|
129
134
|
}
|
|
130
|
-
return { passing, rejected, commented }
|
|
135
|
+
return { passing, rejected, commented, reviewers }
|
|
131
136
|
}
|
|
137
|
+
type Reviewer = { login: string; status: PublishedReview['status']; passes?: true; comment?: true }
|
|
132
138
|
|
|
133
139
|
// A version as the client is told it: what its reviews are held to left
|
|
134
140
|
// out, but whether it is patched.
|
|
@@ -148,7 +154,8 @@ function askedOf(pkg: LockedPackage): [string, ReviewedDependency] {
|
|
|
148
154
|
|
|
149
155
|
// A version's review, and whether it wants one (`wants`), neither passing
|
|
150
156
|
// nor needing none, as only one that can be reviewed may.
|
|
151
|
-
type Review = { unsupported: Unsupported } | { baseline: true } | { reviewed: true } | { reviews: number; rejected?: true; commented?: number
|
|
157
|
+
type Review = { unsupported: Unsupported } | { baseline: true } | { reviewed: true } | { reviews: number; rejected?: true; commented?: number; reviewers?: Reviewer[] }
|
|
158
|
+
| Record<string, never>
|
|
152
159
|
interface Judged { review: Review; wants: boolean }
|
|
153
160
|
type Judge = (pkg: LockedPackage, bases: string[] | 'any') => Judged
|
|
154
161
|
interface Context { state: RepoReviewState; reviews: ReviewsOf; baseline: Set<string> | null }
|
|
@@ -157,8 +164,8 @@ function judge(pkg: LockedPackage, bases: string[] | 'any', { state, reviews, ba
|
|
|
157
164
|
if (as === 'skip') return { review: {}, wants: false }
|
|
158
165
|
if (as !== 'version') return { review: { unsupported: as }, wants: false }
|
|
159
166
|
if (baselineKeys(pkg).some(key => baseline?.has(key))) return { review: { baseline: true }, wants: false }
|
|
160
|
-
const { passing, rejected, commented } = stancesOf(pkg, reviews, bases)
|
|
161
|
-
const told = { reviews: passing, ...commented > 0 ? { commented } : {} }
|
|
167
|
+
const { passing, rejected, commented, reviewers } = stancesOf(pkg, reviews, bases)
|
|
168
|
+
const told = { reviews: passing, ...commented > 0 ? { commented } : {}, ...reviewers.length > 0 ? { reviewers } : {} }
|
|
162
169
|
if (rejected) return { review: { ...told, rejected: true }, wants: true }
|
|
163
170
|
return { review: told, wants: passing < state.required }
|
|
164
171
|
}
|
|
@@ -25,13 +25,14 @@ export interface RepoDependencyReview { enabled: boolean; required: number; sinc
|
|
|
25
25
|
// A review published of the version it is of: its package, version and
|
|
26
26
|
// npm's integrity of it, the patch it is of (none here, the app reviewing no
|
|
27
27
|
// patch yet), the version it was compared with (none for a whole package's),
|
|
28
|
-
// who wrote it (`reviewer`, their id, else their login then),
|
|
29
|
-
//
|
|
28
|
+
// who wrote it (`reviewer`, their id, else their login then), their login
|
|
29
|
+
// as it was then (`login`), its verdict, whether it checked every file
|
|
30
|
+
// (`full`), and when it was published.
|
|
30
31
|
// One made in a pull request's comments (`comment`) counts for it alone;
|
|
31
32
|
// one published here has its `id`.
|
|
32
33
|
export interface PublishedReview {
|
|
33
34
|
id?: string; package: string; version: string; integrity: string; patchHash?: string; basePackage: string | null; baseVersion: string | null
|
|
34
|
-
reviewer: string; status: ReviewVerdict; full: boolean; time: number; comment?: true
|
|
35
|
+
reviewer: string; login: string; status: ReviewVerdict; full: boolean; time: number; comment?: true
|
|
35
36
|
}
|
|
36
37
|
// A repository's review in effect for a reader: its settings, whether they
|
|
37
38
|
// publish reviews to one of the teams that count (`canReview`), and the
|
|
@@ -105,7 +106,7 @@ async function publishedReviews(db: ManagedSql, repoId: number, versions: Review
|
|
|
105
106
|
const nonText = Array.isArray(snapshot) ? null : snapshot?.nonTextFiles ?? []
|
|
106
107
|
const full = summarizeReview(entries.get(row.id) ?? [], files, nonText).coverage === 'full'
|
|
107
108
|
found.push({ id: row.id, package: row.package, version: row.version, integrity: row.integrity, basePackage: row.basePackage, baseVersion: row.baseVersion,
|
|
108
|
-
reviewer: row.userId ?? `login:${row.userLogin.toLowerCase()}`, status: row.status, full, time: Number(row.publishedAt) })
|
|
109
|
+
reviewer: row.userId ?? `login:${row.userLogin.toLowerCase()}`, login: row.userLogin, status: row.status, full, time: Number(row.publishedAt) })
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
return found
|
|
@@ -76,18 +76,21 @@ export interface PulseCommit { sha: string; at: number; subject: string; login:
|
|
|
76
76
|
// each commit's own time zone, and, counted from its commits, each period's
|
|
77
77
|
// (`punches`, by its weeks); `lines` false where GitHub counts none, null
|
|
78
78
|
// while it is counting them; counted from its commits, the latest pushed to
|
|
79
|
-
// the branch directly (`directCommits`, as many as DIRECT_LISTED)
|
|
79
|
+
// the branch directly (`directCommits`, as many as DIRECT_LISTED); and,
|
|
80
|
+
// counted from its commits read no further back than a period needed, when
|
|
81
|
+
// from it is counted whole (`from`), absent for the year whole.
|
|
80
82
|
export interface PulseStats { weeks: PulseWeek[]; contributors: PulseContributor[]; punch: number[][]; punches?: Record<number, number[][]>; lines: boolean | null
|
|
81
|
-
directCommits?: PulseCommit[] }
|
|
83
|
+
directCommits?: PulseCommit[]; from?: number }
|
|
82
84
|
// The lines added and removed a week, and each contributor's, as GitHub's
|
|
83
85
|
// statistics count them, `lines` false where it counts none.
|
|
84
86
|
export interface PulseLines { weeks: [number, number, number][]; contributors: { login: string; weeks: [number, number, number][] }[]; lines: boolean }
|
|
85
87
|
// The days (`from`, `to`, both days, UTC; `to` none, on) and the authors (GitHub
|
|
86
88
|
// logins, any of them) a period's pull requests and issues are narrowed to.
|
|
87
89
|
export interface PulseNarrowing { from: number; to: number | null; authors: string[] }
|
|
88
|
-
// The year's commits read so far: all of them
|
|
89
|
-
// (`
|
|
90
|
-
|
|
90
|
+
// The year's commits read so far: all of them, or as far back as asked
|
|
91
|
+
// (`commits`, `from`), else how many (`read`), and whether past
|
|
92
|
+
// YEAR_COMMITS (`tooMany`).
|
|
93
|
+
export type YearCommits = { complete: true; commits: HistoryCommit[]; passed: HistoryCommit[]; from: number | null } | { complete: false; read: number; tooMany: boolean }
|
|
91
94
|
|
|
92
95
|
const count = (value: unknown) => Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : 0
|
|
93
96
|
const timeOf = (value: unknown) => {
|
|
@@ -204,17 +207,28 @@ export async function defaultHead(fullName: string, token: string, fetchImpl: ty
|
|
|
204
207
|
// YEAR_QUERIES, the rest left for the next to ask (`complete` false, how
|
|
205
208
|
// many are `read`); more than YEAR_COMMITS, too many (`tooMany`). Read
|
|
206
209
|
// whole, those it was read on past besides (`passed`): undated, or dated
|
|
207
|
-
// before it, a clock behind, the branch's commits since beneath them.
|
|
210
|
+
// before it, a clock behind, the branch's commits since beneath them. Read
|
|
211
|
+
// no further than `need` asks, those since it read whole (`complete`, `from`
|
|
212
|
+
// the newest of those read whose parents are not, every commit after it
|
|
213
|
+
// read; null, the year read whole).
|
|
208
214
|
export async function yearCommits(fullName: string, token: string, head: string, since: number, pages: HistoryPages,
|
|
209
|
-
fetchImpl: typeof fetch = globalThis.fetch): Promise<YearCommits> {
|
|
215
|
+
fetchImpl: typeof fetch = globalThis.fetch, need = since): Promise<YearCommits> {
|
|
210
216
|
const found = new Map<string, HistoryCommit>()
|
|
211
217
|
const tried = new Set<string>()
|
|
212
218
|
const within = (commit: HistoryCommit) => commit.committedAt === null || commit.committedAt >= since - YEAR_SKEW_MS
|
|
213
219
|
const dated = (commit: HistoryCommit) => commit.committedAt !== null && commit.committedAt >= since
|
|
214
220
|
const read = () => [...found.values()].filter(dated).length
|
|
221
|
+
const passed = () => [...found.values()].filter(commit => !dated(commit) && within(commit))
|
|
215
222
|
let queries = 0
|
|
216
223
|
let starts = [head]
|
|
217
224
|
while (starts.length > 0) {
|
|
225
|
+
// Those not read yet, the head before any, else the parents of those
|
|
226
|
+
// read, no later than the newest of them (but for a clock behind): read
|
|
227
|
+
// whole after it.
|
|
228
|
+
const unread = new Set(starts)
|
|
229
|
+
const from = found.size === 0 ? Number.POSITIVE_INFINITY : Math.max(Number.NEGATIVE_INFINITY, ...[...found.values()]
|
|
230
|
+
.filter(commit => commit.parents.some(sha => unread.has(sha))).map(commit => commit.committedAt ?? Number.POSITIVE_INFINITY))
|
|
231
|
+
if (need > since && from < need) return { complete: true, commits: [...found.values()].filter(dated), passed: passed(), from }
|
|
218
232
|
if (queries >= YEAR_QUERIES) return { complete: false, read: read(), tooMany: false }
|
|
219
233
|
const batch = starts.slice(0, YEAR_STARTS)
|
|
220
234
|
for (const sha of batch) tried.add(sha)
|
|
@@ -224,7 +238,7 @@ export async function yearCommits(fullName: string, token: string, head: string,
|
|
|
224
238
|
if (read() > YEAR_COMMITS) return { complete: false, read: read(), tooMany: true }
|
|
225
239
|
starts = [...new Set([...found.values()].filter(within).flatMap(commit => commit.parents))].filter(sha => !found.has(sha) && !tried.has(sha))
|
|
226
240
|
}
|
|
227
|
-
return { complete: true, commits: [...found.values()].filter(dated), passed:
|
|
241
|
+
return { complete: true, commits: [...found.values()].filter(dated), passed: passed(), from: null }
|
|
228
242
|
}
|
|
229
243
|
|
|
230
244
|
// Whether an author is a GitHub App's bot: by its login (`<app>[bot]`, as a
|