@d3lm/lint-preset 1.4.5 → 1.5.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/lint-preset/lint-preset/dist/comments.cjs","../src/comments/analyze.ts","../src/comments/style.ts","../src/comments/wrap.ts","../src/comments/agent.ts","../src/comments/config.ts"],"names":["balanced","structure","readFileSync"],"mappings":"AAAA,mkCAA6B;AAC7B;AACA;ACFA,uCAAyD;ADIzD;AACA;AEgBA,IAAM,OAAA,EAAS,wDAAA;AAMf,IAAM,YAAA,EAAc,0BAAA;AAOpB,IAAM,KAAA,EAAO,uDAAA;AACb,IAAM,UAAA,EAAY,WAAA;AAOlB,IAAM,SAAA,EAAW,uFAAA;AAEV,IAAM,YAAA,EAAc,uBAAA;AAMpB,IAAM,UAAA,EAAY,uCAAA;AAUzB,IAAM,MAAA,EACJ,kJAAA;AAEK,SAAS,mBAAA,CAAoB,IAAA,EAAc,OAAA,EAAiB,WAAA,EAA6B;AAC9F,EAAA,OAAO,IAAA,CAAK,UAAA,CAAW,OAAA,EAAS,CAAC,KAAA,EAAA,GAAU,CAAA,EAAA;AAC7C;AAEoD;AACT,EAAA;AAEJ,EAAA;AACxB,IAAA;AAC8B,IAAA;AAC3C,EAAA;AAGc,EAAA;AAEhB;AAEoD;AACN,EAAA;AAC9C;AAEgD;AACP,EAAA;AAEG,EAAA;AAC5C;AAE2E;AAC3C,EAAA;AACS,EAAA;AAMA,EAAA;AAEF,EAAA;AACI,IAAA;AAEQ,IAAA;AACN,MAAA;AACzC,IAAA;AAEyB,IAAA;AACR,MAAA;AACV,IAAA;AACqB,MAAA;AACN,QAAA;AACpB,MAAA;AAEwB,MAAA;AACN,QAAA;AAClB,MAAA;AAEqB,MAAA;AACN,QAAA;AACf,MAAA;AAE+B,MAAA;AACT,QAAA;AACtB,MAAA;AAEyB,MAAA;AACN,QAAA;AACnB,MAAA;AACF,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AFtE8C;AACA;AGhC3B;AACE;AAMS;AAC5B,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACD;AAEqD;AAGjD,EAAA;AAG+B,EAAA;AACpC;AAE+D;AACjD,EAAA;AAEyB,EAAA;AACV,IAAA;AACd,MAAA;AACqB,IAAA;AACrB,MAAA;AAEQ,MAAA;AACR,QAAA;AACT,MAAA;AACF,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAE0D;AAC9C,EAAA;AAEgB,EAAA;AACjB,IAAA;AACT,EAAA;AAEa,EAAA;AACf;AAM8D;AACzB,EAAA;AAEQ,EAAA;AACH,IAAA;AAElB,IAAA;AACM,MAAA;AAC1B,IAAA;AAEwC,IAAA;AAC1C,EAAA;AAEO,EAAA;AACT;AAKiD;AACrB,EAAA;AAEZ,EAAA;AACF,EAAA;AAEgB,EAAA;AACH,IAAA;AACM,IAAA;AAGK,IAAA;AACG,MAAA;AAEK,MAAA;AAC/B,MAAA;AAET,MAAA;AACF,IAAA;AAEkB,IAAA;AACsB,MAAA;AACtB,MAAA;AAEhB,MAAA;AACF,IAAA;AAEqB,IAAA;AACN,MAAA;AACQ,QAAA;AAET,QAAA;AACZ,MAAA;AAES,MAAA;AAET,MAAA;AACF,IAAA;AAEW,IAAA;AACF,IAAA;AACX,EAAA;AAEa,EAAA;AACQ,IAAA;AACrB,EAAA;AAEO,EAAA;AACT;AAEgD;AAClC,EAAA;AAE6B,EAAA;AAChB,IAAA;AAEJ,IAAA;AACR,MAAA;AAET,MAAA;AACF,IAAA;AAE6B,IAAA;AAEX,IAAA;AACR,MAAA;AACe,IAAA;AACd,MAAA;AAC+B,IAAA;AACjC,MAAA;AACT,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAEiD;AACf,EAAA;AAClC;AAO6F;AACrD,EAAA;AAEK,EAAA;AACN,IAAA;AACG,MAAA;AAEzB,MAAA;AACgB,MAAA;AAEQ,MAAA;AACJ,QAAA;AAC/B,MAAA;AAEmC,MAAA;AACA,QAAA;AAEzB,QAAA;AAER,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAEuF;AAC1C,EAAA;AAClB,EAAA;AAEkB,EAAA;AACA,IAAA;AAEA,IAAA;AAE3B,IAAA;AAChB,EAAA;AAEO,EAAA;AACT;AAE4C;AACR,EAAA;AACpC;AAE8C;AACR,EAAA;AACtC;AAE4C;AACX,EAAA;AACjC;AAE4D;AAClB,EAAA;AAC1C;AAQoD;AACR,EAAA;AAC5C;AAIqE;AAE1C,EAAA;AAEmB,EAAA;AACP,IAAA;AACG,MAAA;AAC/B,IAAA;AACW,MAAA;AAClB,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAEoE;AAC1B,EAAA;AAC1C;AAEuD;AACxC,EAAA;AAEe,EAAA;AACV,IAAA;AAClB,EAAA;AAE4C,EAAA;AAC9C;AASE;AAGuC,EAAA;AACzC;AAWE;AAI2B,EAAA;AAEL,EAAA;AACT,EAAA;AAEyB,EAAA;AACV,IAAA;AAEF,IAAA;AACa,MAAA;AACG,MAAA;AACE,MAAA;AAEP,MAAA;AAC/B,QAAA;AACF,MAAA;AAEyB,MAAA;AAEe,MAAA;AACR,QAAA;AAChC,MAAA;AAEe,MAAA;AAEe,MAAA;AACrB,QAAA;AACT,MAAA;AAEO,MAAA;AACkB,MAAA;AAC3B,IAAA;AAEe,IAAA;AAES,IAAA;AAC1B,EAAA;AAEqB,EAAA;AACJ,IAAA;AACjB,EAAA;AAEO,EAAA;AACT;AAE4B;AAE6C;AACzC,EAAA;AACc,EAAA;AAER,EAAA;AACtC;AAewB;AACW;AA4DyC;AAC5C,EAAA;AACS,EAAA;AAE7B,EAAA;AAE8B,EAAA;AACG,IAAA;AAChC,MAAA;AACT,IAAA;AAE8B,IAAA;AAChC,EAAA;AAEO,EAAA;AACT;AAM8E;AAClE,EAAA;AACiB,EAAA;AAER,EAAA;AACyB,IAAA;AAEb,IAAA;AACpB,MAAA;AACF,IAAA;AACU,MAAA;AACjB,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAYE;AAGgC,EAAA;AACW,EAAA;AACX,EAAA;AAGC,EAAA;AACW,EAAA;AACnB,EAAA;AACE,EAAA;AAEjB,EAAA;AACC,EAAA;AAEiC,EAAA;AACR,IAAA;AAGO,IAAA;AACvC,MAAA;AACF,IAAA;AAEW,IAAA;AACE,IAAA;AAEgB,IAAA;AACY,MAAA;AAEJ,MAAA;AACG,QAAA;AAEf,QAAA;AACnB,UAAA;AACF,QAAA;AAEsC,QAAA;AAEV,QAAA;AAC1B,UAAA;AACF,QAAA;AAEqC,QAAA;AAEd,QAAA;AACd,UAAA;AACE,UAAA;AACX,QAAA;AACF,MAAA;AACF,IAAA;AAEe,IAAA;AACI,IAAA;AAEI,IAAA;AACI,MAAA;AAClB,MAAA;AACT,IAAA;AACF,EAAA;AAE+C,EAAA;AACjD;AAUE;AAMqB,EAAA;AACJ,EAAA;AAEH,EAAA;AAEc,EAAA;AACe,IAAA;AAED,IAAA;AAC1C,EAAA;AAE4B,EAAA;AAC1B,IAAA;AACA,IAAA;AACoB,IAAA;AACe,IAAA;AACG,IAAA;AACG,MAAA;AACxC,IAAA;AACD,IAAA;AACF,EAAA;AAE2B,EAAA;AAEiB,EAAA;AACvB,IAAA;AACrB,EAAA;AAG2C,EAAA;AAEvC,EAAA;AAMuC,EAAA;AACL,IAAA;AACC,IAAA;AAGE,IAAA;AAEM,IAAA;AAC3C,MAAA;AACF,IAAA;AAEyC,IAAA;AACS,IAAA;AAET,IAAA;AACA,MAAA;AAE7B,MAAA;AACR,QAAA;AACF,MAAA;AAEa,MAAA;AACf,IAAA;AAEyB,IAAA;AACvB,MAAA;AACF,IAAA;AAE0B,IAAA;AAEa,IAAA;AACG,MAAA;AAEA,MAAA;AAEA,MAAA;AACtC,QAAA;AACF,MAAA;AAEqB,MAAA;AAEgB,MAAA;AACD,QAAA;AACpC,MAAA;AAEqC,MAAA;AACvC,IAAA;AACF,EAAA;AAEW,EAAA;AACF,IAAA;AACT,EAAA;AAEwC,EAAA;AAED,EAAA;AACzC;AAKE;AAGsC,EAAA;AACD,IAAA;AAEP,IAAA;AACA,MAAA;AACxB,QAAA;AACoC,QAAA;AACtC,MAAA;AAEoC,MAAA;AAEA,MAAA;AACrB,QAAA;AACX,UAAA;AACqB,UAAA;AACD,UAAA;AACN,UAAA;AAChB,QAAA;AAEmC,QAAA;AACrB,UAAA;AACd,QAAA;AACF,MAAA;AAEuC,MAAA;AACxB,QAAA;AAEkB,QAAA;AACD,UAAA;AACE,UAAA;AAEnB,UAAA;AAEsB,UAAA;AACF,YAAA;AAEV,YAAA;AAEI,YAAA;AACzB,UAAA;AAEO,UAAA;AACR,QAAA;AACH,MAAA;AAEuB,MAAA;AACY,MAAA;AAEP,MAAA;AACO,QAAA;AAEG,QAAA;AACN,UAAA;AAC9B,QAAA;AACF,MAAA;AAEqC,MAAA;AAEG,MAAA;AACrB,QAAA;AACf,UAAA;AACqB,UAAA;AACD,UAAA;AACpB,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AAEc,QAAA;AACLA,UAAAA;AACT,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAEkC,EAAA;AAEX,EAAA;AACd,IAAA;AACT,EAAA;AAE2B,EAAA;AACV,IAAA;AACwB,IAAA;AACX,IAAA;AAEN,IAAA;AACb,MAAA;AACT,IAAA;AACF,EAAA;AAEuC,EAAA;AAEzB,EAAA;AACL,IAAA;AACT,EAAA;AAEqC,EAAA;AAEC,EAAA;AACxC;AAQE;AAImC,EAAA;AAC1B,IAAA;AACT,EAAA;AAE0B,EAAA;AAGC,EAAA;AAEgB,EAAA;AACR,IAAA;AACF,IAAA;AAK5B,IAAA;AAKoB,MAAA;AACvB,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AASE;AAG6B,EAAA;AACH,EAAA;AAEf,EAAA;AAE+B,EAAA;AACC,IAAA;AACX,IAAA;AAChB,IAAA;AAED,IAAA;AAE2B,IAAA;AACA,MAAA;AACxC,IAAA;AAEuC,IAAA;AACrC,MAAA;AACF,IAAA;AAEyC,IAAA;AACH,MAAA;AACtC,IAAA;AAMoC,IAAA;AAED,IAAA;AACM,MAAA;AACzC,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AH/U8C;AACA;ACreI;AACtC,EAAA;AACC,EAAA;AACJ,EAAA;AACK,EAAA;AACJ,EAAA;AACA,EAAA;AACG,EAAA;AACA,EAAA;AACI,EAAA;AACL,EAAA;AACJ,EAAA;AACR;AA+FyB;AAEX;AACI;AACS;AAMA;AACzB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACD;AAG0B;AAGC;AAQ1B;AAOsB;AAOF;AAGH;AAEC;AAGI;AAEA;AAGL;AAyBiD;AACvB,EAAA;AACJ,IAAA;AACvC,EAAA;AAEyC,EAAA;AAChC,IAAA;AACT,EAAA;AAEuB,EAAA;AACd,IAAA;AACT,EAAA;AAEqB,EAAA;AAEM,EAAA;AACD,IAAA;AAC1B,EAAA;AAEwB,EAAA;AACS,IAAA;AACjC,EAAA;AAE2C,EAAA;AAC7C;AAEmD;AAChB,EAAA;AACxB,IAAA;AACT,EAAA;AAGe,EAAA;AAAoC;AAAA,CAAA;AACb,EAAA;AACL,EAAA;AAEU,EAAA;AAClC,IAAA;AACT,EAAA;AAEgC,EAAA;AAEY,EAAA;AAC9C;AAE4B;AAC1B,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACD;AAMkD;AACxB,EAAA;AAChB,IAAA;AACT,EAAA;AAEsC,EAAA;AAAyB;AAAM,EAAA;AAClC,EAAA;AAEG,EAAA;AAC7B,IAAA;AACT,EAAA;AAEqB,EAAA;AAEM,EAAA;AACD,IAAA;AAC1B,EAAA;AAEwB,EAAA;AACf,IAAA;AACT,EAAA;AAEuB,EAAA;AAEa,EAAA;AAC3B,IAAA;AACT,EAAA;AAIO,EAAA;AAGT;AAKmD;AACX,EAAA;AAEH,EAAA;AACrC;AAM2C;AAGxB,EAAA;AAKnB;AAEsE;AACvC,EAAA;AACpB,IAAA;AACT,EAAA;AAE6B,EAAA;AACpB,IAAA;AACT,EAAA;AAGmB,EAAA;AACV,IAAA;AACT,EAAA;AAEO,EAAA;AACT;AAO0C;AACN,EAAA;AACpC;AAEoC;AACE,EAAA;AACtC;AAEwC;AACM,EAAA;AAEP,EAAA;AACxB,IAAA;AAC+B,IAAA;AAC5C,EAAA;AAEO,EAAA;AACT;AAMgE;AAClB,EAAA;AACf,EAAA;AAEF,EAAA;AACiB,IAAA;AAEH,IAAA;AAC9B,MAAA;AACT,IAAA;AAE8B,IAAA;AACb,MAAA;AACjB,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAEwE;AAC7B,EAAA;AACE,EAAA;AAMD,EAAA;AAGD,EAAA;AACD,IAAA;AACxC,EAAA;AAEI,EAAA;AACW,EAAA;AAGuB,EAAA;AAED,EAAA;AACK,IAAA;AAEH,IAAA;AAGR,IAAA;AAChB,MAAA;AACe,IAAA;AACf,MAAA;AAC6B,IAAA;AAC7B,MAAA;AAC2B,IAAA;AAC3B,MAAA;AACb,IAAA;AACF,EAAA;AAEsC,EAAA;AAE1B,EAAA;AAC8B,IAAA;AAC1C,EAAA;AAEc,EAAA;AACwB,IAAA;AACtC,EAAA;AAM2C,EAAA;AAC9B,IAAA;AACb,EAAA;AAE0C,EAAA;AAC5C;AAOmB;AACT,EAAA;AAEY,EAAA;AACI,IAAA;AACxB,EAAA;AAE4B,EAAA;AACA,IAAA;AAEI,IAAA;AACQ,MAAA;AAEd,MAAA;AACxB,IAAA;AAE0C,IAAA;AAC3B,MAAA;AACf,IAAA;AAEO,IAAA;AACT,EAAA;AACF;AAE6D;AACvB,EAAA;AACL,EAAA;AAE3B,EAAA;AACY,EAAA;AAEU,EAAA;AACX,IAAA;AACyB,MAAA;AAChB,QAAA;AACpB,MAAA;AAEuB,MAAA;AAEb,MAAA;AACZ,IAAA;AACF,EAAA;AAEiE,EAAA;AACzD,IAAA;AAEmC,IAAA;AAC3C,EAAA;AAE0B,EAAA;AACP,IAAA;AAEF,IAAA;AAK2B,MAAA;AACvB,QAAA;AACS,wBAAA;AAExB,QAAA;AACF,MAAA;AAEY,MAAA;AACd,IAAA;AAEsB,IAAA;AACI,sBAAA;AACT,MAAA;AAEQ,MAAA;AACf,QAAA;AACR,MAAA;AAEA,MAAA;AACF,IAAA;AAEmB,IAAA;AACkB,MAAA;AAC7B,MAAA;AAEN,MAAA;AACF,IAAA;AAEkB,IAAA;AACV,MAAA;AAEN,MAAA;AACF,IAAA;AAEsB,IAAA;AACgB,MAAA;AAEpC,MAAA;AACF,IAAA;AAEsB,IAAA;AACiB,MAAA;AACT,MAAA;AAEhB,MAAA;AACmB,QAAA;AACS,MAAA;AACf,QAAA;AAClB,MAAA;AAC6B,QAAA;AACpC,MAAA;AAEA,MAAA;AACF,IAAA;AAE+B,IAAA;AAEtB,IAAA;AAC+B,MAAA;AAEN,MAAA;AAEhC,MAAA;AACF,IAAA;AAEoC,IAAA;AAExB,IAAA;AACmB,MAAA;AAE7B,MAAA;AACF,IAAA;AAGiC,IAAA;AACR,MAAA;AAEvB,MAAA;AACF,IAAA;AAEmB,IAAA;AACrB,EAAA;AAEM,EAAA;AAEC,EAAA;AACT;AAEsE;AACxB,EAAA;AAC9C;AAE8D;AACpB,EAAA;AAC1C;AAEoE;AAExB,EAAA;AAC5C;AAM4F;AAC3D,EAAA;AACW,EAAA;AAEhB,EAAA;AACc,IAAA;AAEjB,IAAA;AACnB,MAAA;AACF,IAAA;AAEoC,IAAA;AACM,MAAA;AAExC,MAAA;AACF,IAAA;AAE0C,IAAA;AACH,IAAA;AAEA,IAAA;AACG,MAAA;AAC1C,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAE0F;AAChD,EAAA;AAC1C;AAEgD;AAG3C,EAAA;AAEL;AAEuF;AACzC,EAAA;AAEA,EAAA;AAClC,IAAA;AACV,EAAA;AAEwC,EAAA;AACI,IAAA;AAEN,IAAA;AACrC,EAAA;AAEsC,EAAA;AACzC;AAEsF;AAChE,EAAA;AACV,IAAA;AACV,EAAA;AAE4C,EAAA;AAEA,EAAA;AACR,IAAA;AACnC,EAAA;AAEsC,EAAA;AACD,IAAA;AAEC,IAAA;AACtC,EAAA;AACH;AAE0D;AAC9C,EAAA;AAEwB,EAAA;AACzB,IAAA;AACT,EAAA;AAE0C,EAAA;AACjC,IAAA;AACT,EAAA;AAEyB,EAAA;AAChB,IAAA;AACT,EAAA;AAE2C,EAAA;AAC7C;AAM+E;AAC7C,EAAA;AAElB,EAAA;AACsB,IAAA;AACF,IAAA;AACE,IAAA;AACnC,EAAA;AACH;AAE+D;AAC1B,EAAA;AAEM,EAAA;AAC3C;AAEmG;AACxD,EAAA;AAC3C;AAE6F;AAC5D,EAAA;AACQ,EAAA;AACF,EAAA;AAEN,EAAA;AACtB,IAAA;AACF,MAAA;AACS,MAAA;AACM,MAAA;AACqB,MAAA;AAC3B,MAAA;AACd,IAAA;AACF,EAAA;AAE0C,EAAA;AACA,IAAA;AACV,IAAA;AAEO,IAAA;AACvC,EAAA;AAEgC,EAAA;AACC,IAAA;AAEjB,IAAA;AACT,MAAA;AACqC,MAAA;AAC1C,IAAA;AAEkB,IAAA;AACU,MAAA;AACA,QAAA;AACzB,MAAA;AACH,IAAA;AAEiCC,IAAAA;AAEI,IAAA;AACvC,EAAA;AAEwD,EAAA;AACjBA,IAAAA;AAEA,IAAA;AACvC,EAAA;AAEwC,EAAA;AAEzB,EAAA;AACQ,IAAA;AACvB,EAAA;AAEqB,EAAA;AACqB,EAAA;AACD,EAAA;AACN,EAAA;AACD,EAAA;AAEG,EAAA;AACpB,IAAA;AACjB,EAAA;AAE4C,EAAA;AAEtB,EAAA;AACjB,IAAA;AAGG,IAAA;AAEyB,IAAA;AACG,IAAA;AACpC,EAAA;AAE4C,EAAA;AACV,IAAA;AAER,IAAA;AAC1B,EAAA;AAEqC,EAAA;AAGE,EAAA;AACK,EAAA;AAMI,EAAA;AACX,IAAA;AACrC,EAAA;AAG4C,EAAA;AACJ,IAAA;AACxC,EAAA;AAEoC,EAAA;AAEkC,EAAA;AAC9B,IAAA;AACH,MAAA;AAEE,MAAA;AACpC,IAAA;AACH,EAAA;AAQS,EAAA;AACoB,IAAA;AACI,IAAA;AAElB,IAAA;AACX,MAAA;AACF,IAAA;AAGmC,IAAA;AACK,IAAA;AACE,IAAA;AAED,IAAA;AACP,MAAA;AAEM,MAAA;AACxC,IAAA;AAE0C,IAAA;AACT,IAAA;AACnC,EAAA;AAE0C,EAAA;AAET,EAAA;AACoB,EAAA;AAEjC,EAAA;AACiB,IAAA;AAEC,IAAA;AACF,MAAA;AAClC,IAAA;AAEW,IAAA;AACuB,MAAA;AACJ,QAAA;AAC5B,MAAA;AACF,IAAA;AAEwC,IAAA;AACC,MAAA;AACzC,IAAA;AAQW,IAAA;AAG8B,MAAA;AAEJ,MAAA;AACrC,IAAA;AACF,EAAA;AAE2B,EAAA;AACQ,IAAA;AACnC,EAAA;AAEmC,EAAA;AACI,EAAA;AAEK,EAAA;AAC9C;AAEqD;AAClC,EAAA;AAE0B,EAAA;AACD,IAAA;AACjB,MAAA;AACvB,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAEuE;AAC3D,EAAA;AACqB,EAAA;AAEZ,EAAA;AACwB,IAAA;AAEP,IAAA;AAC1B,MAAA;AACD,IAAA;AACW,MAAA;AAClB,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AASmD;AACN,EAAA;AAC7C;AAKE;AAEqC,EAAA;AACM,EAAA;AACW,EAAA;AACX,EAAA;AAES,EAAA;AACd,IAAA;AACE,MAAA;AACrC,IAAA;AAEkC,IAAA;AACrC,EAAA;AAE2C,EAAA;AAGA,EAAA;AACb,IAAA;AAC9B,EAAA;AAEkC,EAAA;AACO,IAAA;AACzC,EAAA;AAEkC,EAAA;AACzB,IAAA;AACL,MAAA;AACwB,MAAA;AAC1B,IAAA;AACF,EAAA;AAE8B,EAAA;AACf,IAAA;AACf,EAAA;AAE0B,EAAA;AACK,EAAA;AAEY,EAAA;AACF,IAAA;AAE3B,IAAA;AACG,MAAA;AACf,IAAA;AAE8B,IAAA;AAEpB,IAAA;AACe,MAAA;AACR,MAAA;AACI,MAAA;AACsB,MAAA;AACV,MAAA;AAChC,IAAA;AACH,EAAA;AAEmC,EAAA;AACrC;AAE2D;AACpB,EAAA;AACE,EAAA;AACI,EAAA;AACA,EAAA;AACH,EAAA;AAEN,EAAA;AACpC;AAKoF;AACxD,EAAA;AAEkB,EAAA;AACH,IAAA;AACzC,EAAA;AAEkC,EAAA;AACzB,IAAA;AACT,EAAA;AAE2C,EAAA;AAED,EAAA;AAC5C;AAE+B;AAKsD;AACvC,EAAA;AAC9C;AAEoG;AAClF,EAAA;AACmB,EAAA;AAEI,EAAA;AACzC;AAOgE;AACvB,EAAA;AACzC;AAMgE;AACf,EAAA;AAC7B,IAAA;AAClB,EAAA;AAG+B,EAAA;AACO,EAAA;AACG,EAAA;AAOH,EAAA;AACA,IAAA;AACtC,EAAA;AAE0C,EAAA;AAGuB,EAAA;AAC/B,EAAA;AAEQ,EAAA;AAEV,IAAA;AAE/B,EAAA;AAE2B,EAAA;AACM,IAAA;AAEH,IAAA;AACnB,MAAA;AAC4B,MAAA;AACtC,IAAA;AAGmB,IAAA;AAImB,IAAA;AAEzB,IAAA;AACL,MAAA;AACM,MAAA;AACiB,MAAA;AACJ,MAAA;AACW,MAAA;AACrB,MAAA;AACG,MAAA;AACW,MAAA;AACR,MAAA;AACa,MAAA;AACnC,IAAA;AACH,EAAA;AAE2B,EAAA;AAC7B;AAK6D;AAC5B,EAAA;AACjC;AAMiE;AAChC,EAAA;AACjC;AAM6D;AACvB,EAAA;AAG1B,EAAA;AAG0B,EAAA;AACE,IAAA;AAEI,IAAA;AAC1C,EAAA;AAEsB,EAAA;AACxB;AAE+B;AAQ2C;AAChC,EAAA;AAEjB,EAAA;AACmB,IAAA;AAC1C,EAAA;AAGY,EAAA;AAAU,IAAA;AACM,IAAA;AAC5B,EAAA;AACF;ADrC8C;AACA;AI/rCxB;AACO;AACZ;AAUY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyCtB;AJwrCuC;AACA;AK9uCzBC;AACS;AACb;AACa;AAoJmB;AAE5B;AACnB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAEyB;AAE4C;AAC5D,EAAA;AACT;AAE6C;AACvC,EAAA;AACuC,IAAA;AAC3B,EAAA;AACE,IAAA;AAClB,EAAA;AACF;AAEkE;AACxB,EAAA;AACd,IAAA;AAC1B,EAAA;AAE2C,EAAA;AAElB,EAAA;AAC3B;AAE+D;AACvB,EAAA;AAEM,EAAA;AACR,IAAA;AACpC,EAAA;AACF;AAE4E;AAChC,EAAA;AACN,IAAA;AACpC,EAAA;AACF;AAE+E;AAC1C,EAAA;AACI,IAAA;AACvC,EAAA;AAEe,EAAA;AAE6B,EAAA;AACN,EAAA;AACH,EAAA;AACI,EAAA;AACC,EAAA;AACJ,EAAA;AACG,EAAA;AACJ,EAAA;AACS,EAAA;AACF,EAAA;AACT,EAAA;AACM,EAAA;AAEC,EAAA;AACd,IAAA;AACkB,IAAA;AAEN,IAAA;AACA,MAAA;AACpC,IAAA;AACF,EAAA;AAEwC,EAAA;AACtB,IAAA;AAClB,EAAA;AAEgC,EAAA;AAEc,EAAA;AAC5B,IAAA;AAClB,EAAA;AAEkC,EAAA;AACF,EAAA;AACG,EAAA;AACC,EAAA;AACI,EAAA;AACX,EAAA;AAGe,EAAA;AAC1B,IAAA;AAClB,EAAA;AAE4C,EAAA;AACnB,IAAA;AAEa,IAAA;AACM,MAAA;AAC1C,IAAA;AACF,EAAA;AAEkC,EAAA;AACO,EAAA;AAEF,EAAA;AACrB,IAAA;AAClB,EAAA;AAE8B,EAAA;AAEY,EAAA;AACxB,IAAA;AAClB,EAAA;AAEO,EAAA;AACT;AAEyG;AAC7E,EAAA;AACW,IAAA;AAEG,IAAA;AACxC,EAAA;AAEgC,EAAA;AAEvB,EAAA;AAC0B,IAAA;AACO,MAAA;AAEZ,MAAA;AACc,QAAA;AACxC,MAAA;AACF,IAAA;AAEkC,IAAA;AAED,IAAA;AACsC,MAAA;AAE5C,MAAA;AACS,QAAA;AAClC,MAAA;AACF,IAAA;AAEqC,IAAA;AAEA,IAAA;AACf,MAAA;AACtB,IAAA;AAEY,IAAA;AACd,EAAA;AACF;ALsjC8C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/lint-preset/lint-preset/dist/comments.cjs","sourcesContent":[null,"import { parseSync, type OxcError, type Statement } from 'oxc-parser';\nimport {\n CODE_SPAN,\n findStyleIssues,\n isBanner,\n LIST_MARKER,\n replaceKeepingLines,\n stripNonProse,\n type StyleIssueType,\n} from './style.js';\nimport {\n bindLineStarts,\n breakAfterSentence,\n findLineIssues,\n hasWidow,\n layoutTokens,\n startsBlock,\n tokenize,\n type LineIssue,\n type LineIssueType,\n type WrapOptions,\n} from './wrap.js';\n\nexport type CommentKind = 'jsdoc' | 'block';\n\nexport type ParagraphKind = 'prose' | 'list' | 'tag' | 'code' | 'other';\n\nexport interface AnalyzeOptions extends WrapOptions {\n /**\n * Sets the preferred line length, including indentation and the comment prefix.\n */\n readonly width: number;\n\n /**\n * Sets the hard limit for measured prose. Parentheticals may exceed `width` up to this limit.\n * When omitted, the existing width and tolerance behavior is retained.\n */\n readonly maxWidth?: number;\n\n /**\n * Sets the maximum length of the line that holds a comment whose whole text is one\n * prose paragraph. Such a comment stays on one line up to this length, even past\n * `width`. It defaults to `width`.\n */\n readonly singleLineWidth?: number;\n\n readonly tabWidth: number;\n\n /**\n * Whether to report prose style issues like semicolons, em-dashes, and section banners.\n */\n readonly style: boolean;\n}\n\nexport type IssueType = LineIssueType | StyleIssueType;\n\nexport type IssueTier = 'verdict' | 'hint';\n\nconst ISSUE_TIERS: Record<IssueType, IssueTier> = {\n overflow: 'verdict',\n underfill: 'verdict',\n widow: 'verdict',\n unbalanced: 'verdict',\n orphan: 'hint',\n banner: 'hint',\n 'em-dash': 'hint',\n semicolon: 'hint',\n 'label-colon': 'hint',\n emphasis: 'hint',\n dash: 'hint',\n};\n\nexport interface IssueReport {\n readonly type: IssueType;\n readonly tier: IssueTier;\n readonly line: number;\n readonly length: number;\n}\n\nexport interface ParagraphReport {\n readonly kind: ParagraphKind;\n readonly startLine: number;\n readonly endLine: number;\n\n /**\n * Whether the paragraph is prose that can be rewrapped. When `false`, `reason` says why.\n */\n readonly reflowable: boolean;\n readonly reason?: string;\n readonly issues: readonly IssueReport[];\n\n /**\n * Holds the exact source lines for `startLine` through `endLine` when the paragraph is\n * greedily wrapped to the width, or `null` when the paragraph isn't prose, e.g. code,\n * a table, a diagram, or `@example`.\n */\n readonly suggestion: readonly string[] | null;\n}\n\nexport interface CommentReport {\n readonly file: string;\n readonly kind: CommentKind;\n readonly startLine: number;\n readonly endLine: number;\n readonly indent: string;\n readonly width: number;\n readonly maxWidth?: number;\n\n /**\n * Holds the length of each source line from `startLine` through `endLine`, with tabs expanded.\n */\n readonly lengths: readonly number[];\n readonly text: string;\n readonly paragraphs: readonly ParagraphReport[];\n}\n\nexport interface SyntaxFailure {\n readonly message: string;\n\n /**\n * Holds the 1-based line of the error, when the parser names one.\n */\n readonly line?: number;\n}\n\nexport interface SourceAnalysis {\n readonly comments: CommentReport[];\n\n /**\n * Describes the syntax error that the parser stopped at. The parser never sees the comments after\n * one, so such a source has no comments at all instead of only those before the error.\n */\n readonly syntaxError?: SyntaxFailure;\n}\n\ninterface BodyLine {\n /**\n * Holds the 0-based index of the line in the file.\n */\n readonly index: number;\n readonly prefix: string;\n readonly text: string;\n readonly verbatim: boolean;\n\n /**\n * Whether the source line ends with Markdown hard line break syntax, which `text` no longer\n * holds after trimming.\n */\n readonly hardBreak: boolean;\n}\n\ninterface RawParagraph {\n kind: ParagraphKind;\n lines: BodyLine[];\n marker: string;\n reason?: string;\n}\n\ninterface ParsedComment {\n readonly type: 'Line' | 'Block';\n readonly value: string;\n readonly start: number;\n readonly end: number;\n}\n\nconst IGNORE_DIRECTIVE = 'comments-ignore';\n\nconst FENCE = /^(?:`{3,}(?!.*`)|~{3,})/;\nconst JSDOC_TAG = /^@([A-Za-z]+)/;\nconst NESTED_LIST_MARKER = /^\\s+(?:[-*+]|\\d+[.)])\\s+/;\n\n/**\n * Tags whose content is a prose description, while others like `@example` hold code and\n * are never rewrapped.\n */\nconst PROSE_TAGS = new Set([\n 'arg',\n 'argument',\n 'deprecated',\n 'description',\n 'exception',\n 'note',\n 'param',\n 'prop',\n 'property',\n 'remarks',\n 'return',\n 'returns',\n 'see',\n 'since',\n 'summary',\n 'template',\n 'throws',\n 'todo',\n 'typeParam',\n 'yields',\n]);\n\n// tags whose content is code that runs until the next tag, empty lines included, like a multi-part `@example`\nconst CODE_TAGS = new Set(['default', 'defaultValue', 'example']);\n\n// tags followed by a name, as in `@param {Type} name - description`\nconst NAMED_TAGS = new Set(['arg', 'argument', 'param', 'prop', 'property', 'template', 'typeParam']);\n\n/**\n * Tool directives take effect in any comment line, so a JSDoc line that starts with one is kept\n * as it is. Each is the exact name of a directive, since prose can start a line with a package\n * name like `eslint-config-prettier`.\n */\nconst LINE_DIRECTIVE =\n /^\\s*(?:(?:eslint|oxlint|stylelint)-(?:disable(?:-next-line|-line)?|enable)\\b|eslint-env\\b|@ts-(?:ignore|expect-error|nocheck|check)\\b|@jsx\\w*\\b|@vite-ignore\\b|[@#]__[A-Z_]+__|@(?:vitest|jest)-environment\\b|(?:istanbul|c8|v8)\\s+ignore\\b|prettier-ignore\\b|biome-ignore\\b|deno-(?:lint-ignore|fmt-ignore)(?:-file)?\\b|tslint:|(?:c[sS]pell|spell-?checker)\\s*:|(?:webpack|turbopack)[A-Z][a-zA-Z]*\\s*:|vim?:\\s|#(?:region|endregion)\\b|<reference\\s)/;\n\n/**\n * These directives are only read from a whole block comment, like `/* global foo *\\/`\n * or `/* eslint quotes: 1 *\\/`. Their markers are ordinary words, so they never count\n * as directives on a single line of a longer comment.\n */\nconst BLOCK_DIRECTIVE = /^\\s*(?:eslint\\s|globals?\\s|exported\\s|jshint\\s|jslint\\s|type\\s*:)/;\n\n/**\n * Legal comments are kept as they are. A comment is legal when it uses the same markers\n * as bundlers like esbuild (`/*!`, `//!`, `@license`, and `@preserve`), or when it is a\n * copyright notice like `Copyright (c) 2020 Foo`.\n */\nconst LEGAL_COMMENT = /^!|@license\\b|@preserve\\b|\\bcopyright\\s+(?:\\(c\\)|©|\\d{4})/i;\n\n// a term of one or two words or a code span (stripped to two backticks), then a dash or colon and its definition\nconst DEFINITION = /^(?:``|[\\w$.[\\]]+(?:[ -][\\w$.[\\]]+)?)(?: [-–]|:) \\S/;\n\nconst BOX_DRAWING = /[\\u2500-\\u257F]/;\n\n// a Markdown table delimiter row like `--- | ---` or `|:--|--:|`, which needs no leading pipe\nconst TABLE_DELIMITER = /^\\|?\\s*:?-+:?\\s*(?:\\|\\s*:?-+:?\\s*)+\\|?$/;\n\nconst ARROWS_OR_RULES = /[▲▼◀▶→←↑↓]|-->|<--|\\+-{2,}|-{3,}|={4,}/;\n\n// two or more spaces after text, or a backslash that another backslash doesn't escape\nconst HARD_BREAK = /\\S {2,}$|(?<!\\\\)\\\\(?:\\\\\\\\)*$/;\n\n/**\n * Describes how a paragraph that the tool keeps as written is checked.\n *\n * - `code`: code that the parser accepts, which gets no checks besides banners\n * - `code-like`: a paragraph with a line that may be code, like one that ends with `{`, which gets\n * no style hints, since they would point at the code, but must still fit the width\n * - `layout`: documented layout syntax like a table row, which gets style hints but no verdicts\n * - `manual`: text that is or may be laid out by hand or may be markup, like a Markdown hard line\n * break, arrows, or a heading, which gets style hints and must still fit the width.\n */\ntype StructureTier = 'code' | 'code-like' | 'layout' | 'manual';\n\ninterface Structure {\n readonly tier: StructureTier;\n readonly reason: string;\n}\n\n/**\n * Whether a statement is something prose also parses as, like a single word (`Done`),\n * a code span, a word before a code span (`Returns \\`null\\``, a tagged template), or a\n * label (`Default: refuse`). A label that ends with a semicolon, like the commented-out\n * CSS declaration `display: none;`, is code.\n */\nfunction isProseLike(statement: Statement, source: string): boolean {\n if (statement.type === 'LabeledStatement') {\n return source[statement.end - 1] !== ';';\n }\n\n if (statement.type === 'EmptyStatement') {\n return true;\n }\n\n if (statement.type !== 'ExpressionStatement') {\n return false;\n }\n\n let { expression } = statement;\n\n while (expression.type === 'ParenthesizedExpression') {\n expression = expression.expression;\n }\n\n if (expression.type === 'TaggedTemplateExpression') {\n return expression.tag.type === 'Identifier';\n }\n\n return expression.type === 'Identifier' || expression.type === 'Literal' || expression.type === 'TemplateLiteral';\n}\n\nfunction parsesAsStatements(text: string): boolean {\n if (!/[()[\\]{}=;<>]/.test(text)) {\n return false;\n }\n\n // a generator body accepts `return`, `await`, and `yield`, which are common in commented-out code\n const source = `async function* comment() {\\n${text}\\n}`;\n const { program, errors } = parseSync('comment.tsx', source);\n const wrapper = program.body.at(0);\n\n if (errors.length > 0 || wrapper?.type !== 'FunctionDeclaration' || !wrapper.body) {\n return false;\n }\n\n const statements = wrapper.body.body;\n\n return statements.length > 0 && !statements.every((statement) => isProseLike(statement, source));\n}\n\nconst CODE_VALUES = new Set([\n 'Literal',\n 'ArrayExpression',\n 'ObjectExpression',\n 'CallExpression',\n 'NewExpression',\n 'ArrowFunctionExpression',\n 'FunctionExpression',\n 'UnaryExpression',\n 'MemberExpression',\n]);\n\n/**\n * A lone property whose value is a word or code span, like `Default: refuse`, reads as a label in\n * prose rather than as commented-out options.\n */\nfunction parsesAsProperties(text: string): boolean {\n if (!text.includes(':')) {\n return false;\n }\n\n const { program, errors } = parseSync('comment.ts', `({\\n${text}\\n})`);\n const statement = program.body.at(0);\n\n if (errors.length > 0 || program.body.length !== 1 || statement?.type !== 'ExpressionStatement') {\n return false;\n }\n\n let { expression } = statement;\n\n while (expression.type === 'ParenthesizedExpression') {\n expression = expression.expression;\n }\n\n if (expression.type !== 'ObjectExpression' || expression.properties.length === 0) {\n return false;\n }\n\n const { properties } = expression;\n\n if (!properties.every((property) => property.type === 'Property' && !property.shorthand && !property.method)) {\n return false;\n }\n\n return (\n properties.length > 1 ||\n text.trimEnd().endsWith(',') ||\n properties.some((property) => property.type === 'Property' && CODE_VALUES.has(property.value.type))\n );\n}\n\n/**\n * Prose almost never parses as JavaScript, since two words in a row are a syntax error.\n */\nfunction isCode(texts: readonly string[]): boolean {\n const text = texts.map((line) => line.replace(LIST_MARKER, '')).join('\\n');\n\n return parsesAsStatements(text) || parsesAsProperties(text);\n}\n\n/**\n * A prose clause can end with a semicolon too, which is a style issue.\n * So `;` only ends code with code around it.\n */\nfunction isCodeLine(text: string): boolean {\n return (\n text.endsWith('{') ||\n (text.endsWith(';') && (/[()[\\]{}=<>]/.test(text) || !/\\s/.test(text))) ||\n /^[}\\])]/.test(text) ||\n /^(?:const|let|var)\\s+[\\w$]+\\s*[:=]/.test(text) ||\n /^(?:import|export)\\s.*(?:\\sfrom\\s|[{}])/.test(text)\n );\n}\n\nfunction layoutReason(prose: string, raw: string): string | undefined {\n if (prose.startsWith('|') || TABLE_DELIMITER.test(prose)) {\n return 'table row';\n }\n\n if (BOX_DRAWING.test(prose)) {\n return 'box drawing';\n }\n\n // a rule or border, like the `***` lines of a box, would be joined into the prose around it\n if (isBanner(raw)) {\n return 'repeated characters';\n }\n\n return undefined;\n}\n\n/**\n * Headings, link definitions, HTML, and quotes are markup, since joining them with the next line\n * would turn them into prose. Prose can start a line like that too, like `<T> is the type` or\n * `# of retries`, so markup is only kept as written and still has to fit the width.\n */\nfunction isMarkup(prose: string): boolean {\n return /^#{1,6}\\s/.test(prose) || /^\\[[^\\]]+\\]:/.test(prose) || /^<\\/?[a-z]/i.test(prose) || /^>(?:\\s|$)/.test(prose);\n}\n\nfunction mask(text: string): string {\n return text.replaceAll(/[^\\n]/g, 'x');\n}\n\nfunction maskCode(text: string): string {\n let masked = text.replaceAll(CODE_SPAN, mask);\n\n for (let previous = ''; previous !== masked; ) {\n previous = masked;\n masked = masked.replaceAll(/\\{[^{}]*\\}/g, mask);\n }\n\n return masked;\n}\n\n/**\n * Two spaces after a sentence are an old typing convention, and a gap on a single line is a typo,\n * so neither is a column.\n */\nfunction hasAlignedColumns(lines: readonly BodyLine[]): boolean {\n const masked = maskCode(lines.map((body) => body.text).join('\\n')).split('\\n');\n const seen = new Set<number>();\n\n for (const line of masked) {\n const columns = new Set([...line.matchAll(/[^\\s.!?][^\\S\\n]{2,}(?=\\S)/g)].map((gap) => gap.index + gap[0].length));\n\n if ([...columns].some((column) => seen.has(column))) {\n return true;\n }\n\n for (const column of columns) {\n seen.add(column);\n }\n }\n\n return false;\n}\n\nfunction structureOf(lines: readonly BodyLine[]): Structure | undefined {\n const trimmed = lines.map((line) => line.text.trim()).join('\\n');\n const texts = replaceKeepingLines(trimmed, CODE_SPAN, '``').split('\\n');\n\n /**\n * Types like `{string | null}`, links like `{@link a | b}`, and URLs hold pipes and dashes\n * that aren't layout.\n */\n const proseLines = stripNonProse(trimmed).split('\\n');\n\n // the parser only accepts a whole paragraph as code when it is code, since two words in a row are a syntax error\n if (isCode(lines.map((line) => line.text.trim()))) {\n return { tier: 'code', reason: 'code' };\n }\n\n let layout: string | undefined;\n let codeLike = false;\n\n // a hard break on the last line of a paragraph ends the paragraph instead, as in Markdown\n let manual = lines.slice(0, -1).some((line) => line.hardBreak) ? 'hard line break' : undefined;\n\n for (const [position, line] of texts.entries()) {\n const prose = proseLines[position].trim();\n\n layout ??= layoutReason(prose, lines[position]?.text ?? '');\n\n // a single line that looks like code may be prose, like one that starts with `import them from`\n if (isCodeLine(line.trim())) {\n codeLike = true;\n } else if (isMarkup(prose)) {\n manual ??= 'markup';\n } else if ((prose.match(/\\|/g)?.length ?? 0) >= 2) {\n manual ??= 'pipes';\n } else if (ARROWS_OR_RULES.test(prose)) {\n manual ??= 'arrows or rules';\n }\n }\n\n layout ??= hasAlignedColumns(lines) ? 'aligned columns' : undefined;\n\n if (layout) {\n return { tier: 'layout', reason: layout };\n }\n\n if (codeLike) {\n return { tier: 'code-like', reason: 'code-like line' };\n }\n\n /**\n * One term per line, like `width - the maximum width` above `height - the maximum height`, may be\n * a definition list.\n */\n if (texts.filter((text) => DEFINITION.test(text.trim())).length >= 2) {\n manual ??= 'definition-like lines';\n }\n\n return manual ? { tier: 'manual', reason: manual } : undefined;\n}\n\n/**\n * A Markdown code fence closes only with the character it opened with, repeated at least as\n * often. A tilde fence inside a backtick block, or three backticks inside a four-backtick\n * block, doesn't end it.\n */\nclass FenceTracker {\n private _open: string | undefined;\n\n isInside(): boolean {\n return this._open !== undefined;\n }\n\n next(text: string): boolean {\n const trimmed = text.trim();\n\n if (this._open === undefined) {\n this._open = FENCE.exec(trimmed)?.[0];\n\n return this._open !== undefined;\n }\n\n if (trimmed.startsWith(this._open) && new RegExp(`^\\\\${this._open[0]}+$`).test(trimmed)) {\n this._open = undefined;\n }\n\n return true;\n }\n}\n\nfunction segment(lines: readonly BodyLine[]): RawParagraph[] {\n const paragraphs: RawParagraph[] = [];\n const fence = new FenceTracker();\n\n let current: RawParagraph | undefined;\n let inCodeTag = false;\n\n const flush = (): void => {\n if (current) {\n while (current.lines.length > 1 && !current.lines.at(-1)?.text.trim()) {\n current.lines.pop();\n }\n\n paragraphs.push(current);\n\n current = undefined;\n }\n };\n\n const start = (kind: ParagraphKind, line: BodyLine, marker = '', reason?: string): void => {\n flush();\n\n current = { kind, lines: [line], marker, reason };\n };\n\n for (const line of lines) {\n const { text } = line;\n\n if (inCodeTag) {\n /**\n * A fence inside the example may hold a line like `@decorator()`, which only starts a tag\n * outside of it.\n */\n if (fence.isInside() || !JSDOC_TAG.test(text)) {\n fence.next(text);\n current?.lines.push(line);\n\n continue;\n }\n\n inCodeTag = false;\n }\n\n if (fence.isInside()) {\n current?.lines.push(line);\n fence.next(text);\n\n if (!fence.isInside()) {\n flush();\n }\n\n continue;\n }\n\n if (line.verbatim) {\n start('code', line, '', 'directive');\n flush();\n\n continue;\n }\n\n if (!text.trim()) {\n flush();\n\n continue;\n }\n\n if (fence.next(text)) {\n start('code', line, '', 'code block');\n\n continue;\n }\n\n if (/^\\s/.test(text)) {\n const holdsProse = current?.kind === 'list' || (current?.kind === 'tag' && !current.reason);\n const nested = holdsProse ? NESTED_LIST_MARKER.exec(text) : null;\n\n if (nested) {\n start('list', line, nested[0]);\n } else if (current && (current.kind === 'list' || current.kind === 'tag' || current.reason === 'indented')) {\n current.lines.push(line);\n } else {\n start('code', line, '', 'indented');\n }\n\n continue;\n }\n\n const tag = JSDOC_TAG.exec(text);\n\n if (tag) {\n start('tag', line, '', PROSE_TAGS.has(tag[1]) ? undefined : `@${tag[1]} tag`);\n\n inCodeTag = CODE_TAGS.has(tag[1]);\n\n continue;\n }\n\n const marker = LIST_MARKER.exec(text);\n\n if (marker) {\n start('list', line, marker[0]);\n\n continue;\n }\n\n // like Markdown, an unindented line right after a list item lazily continues it\n if (current && (current.kind === 'prose' || current.kind === 'tag' || current.kind === 'list')) {\n current.lines.push(line);\n\n continue;\n }\n\n start('prose', line);\n }\n\n flush();\n\n return paragraphs;\n}\n\nfunction startsLine(token: string, next: string | undefined): boolean {\n return startsBlock(token) || LINE_DIRECTIVE.test(next === undefined ? token : `${token} ${next}`);\n}\n\nfunction visualLength(text: string, tabWidth: number): number {\n return text.replaceAll('\\t', ' '.repeat(tabWidth)).length;\n}\n\nfunction sourceLineLength(line: BodyLine, tabWidth: number): number {\n // the text holds the indentation for comments without a prefix, like `other` block comments\n return visualLength(`${line.prefix}${line.text.trimEnd()}`, tabWidth);\n}\n\n/**\n * In legacy width/tolerance mode, an indivisible token is exempt because wrapping cannot\n * make it fit.\n */\nfunction overflowIssues(lines: readonly BodyLine[], options: AnalyzeOptions): IssueReport[] {\n const issues: IssueReport[] = [];\n const limit = options.maxWidth ?? options.width + options.tolerance;\n\n for (const line of lines) {\n const length = sourceLineLength(line, options.tabWidth);\n\n if (length <= limit) {\n continue;\n }\n\n if (options.maxWidth !== undefined) {\n issues.push(lineIssue('overflow', line, options));\n\n continue;\n }\n\n const prefixLength = visualLength(`${line.prefix}${/^\\s*/.exec(line.text)?.[0] ?? ''}`, options.tabWidth);\n const tokens = bindLineStarts(tokenize(line.text), startsLine);\n\n if (tokens.length > 1 && tokens.every((token) => prefixLength + token.length <= limit)) {\n issues.push(lineIssue('overflow', line, options));\n }\n }\n\n return issues;\n}\n\nfunction lineIssue(type: IssueType, line: BodyLine, options: AnalyzeOptions): IssueReport {\n return { type, tier: ISSUE_TIERS[type], line: line.index + 1, length: sourceLineLength(line, options.tabWidth) };\n}\n\nfunction stripBlockSyntax(text: string): string {\n return text\n .replace(/^\\s*\\/\\*+/, '')\n .replace(/\\*+\\/\\s*$/, '')\n .replace(/^\\s*\\*(?=\\s|$)/, '');\n}\n\nfunction bannerIssues(paragraph: RawParagraph, options: AnalyzeOptions): IssueReport[] {\n const verbatim = ['code block', 'indented', 'legal comment'];\n\n if (!options.style || (paragraph.reason !== undefined && verbatim.includes(paragraph.reason))) {\n return [];\n }\n\n const banners = paragraph.lines.filter((line) => {\n const text = paragraph.kind === 'other' ? stripBlockSyntax(line.text) : line.text;\n\n return isBanner(text) || (paragraph.kind === 'other' && /^\\s*\\/\\*{3,}|\\*{3,}\\/\\s*$/.test(line.text));\n });\n\n return banners.map((line) => lineIssue('banner', line, options));\n}\n\nfunction styleIssues(paragraph: RawParagraph, options: AnalyzeOptions): IssueReport[] {\n if (!options.style) {\n return [];\n }\n\n const byIndex = new Map(paragraph.lines.map((line) => [line.index, line]));\n\n const lines = paragraph.lines.map((line) => {\n return { index: line.index, text: paragraph.kind === 'other' ? stripBlockSyntax(line.text) : line.text };\n });\n\n return findStyleIssues(lines).flatMap((issue) => {\n const line = byIndex.get(issue.index);\n\n return line ? [lineIssue(issue.type, line, options)] : [];\n });\n}\n\nfunction bindTagHead(tokens: readonly string[]): string[] {\n let end = 1;\n\n if (tokens[end]?.startsWith('{')) {\n end += 1;\n }\n\n if (NAMED_TAGS.has(tokens[0].slice(1)) && end < tokens.length) {\n end += 1;\n }\n\n if (tokens[end] === '-') {\n end += 1;\n }\n\n return [tokens.slice(0, end).join(' '), ...tokens.slice(end)];\n}\n\n/**\n * A paragraph without a structure, like a single-line block comment, is prose that just can't be\n * rewrapped, so it's checked like text laid out by hand.\n */\nfunction keptIssues(paragraph: RawParagraph, structure: Structure | undefined, options: AnalyzeOptions): IssueReport[] {\n const tier = structure?.tier ?? 'manual';\n\n return byLine([\n ...(tier === 'manual' || tier === 'code-like' ? overflowIssues(paragraph.lines, options) : []),\n ...(tier === 'code' || tier === 'code-like' ? [] : styleIssues(paragraph, options)),\n ...bannerIssues(paragraph, options),\n ]);\n}\n\nfunction byLine(issues: readonly IssueReport[]): IssueReport[] {\n const unique = new Map(issues.map((issue) => [`${issue.line}:${issue.type}`, issue]));\n\n return [...unique.values()].toSorted((a, b) => a.line - b.line);\n}\n\nfunction sameLayout(a: readonly (readonly string[])[], b: readonly (readonly string[])[]): boolean {\n return a.length === b.length && a.every((words, position) => words.join(' ') === b[position].join(' '));\n}\n\nfunction analyzeParagraph(paragraph: RawParagraph, options: AnalyzeOptions): ParagraphReport {\n const first = paragraph.lines[0];\n const last = paragraph.lines.at(-1) ?? first;\n const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };\n\n if (paragraph.kind === 'code') {\n return {\n ...base,\n reflowable: false,\n reason: paragraph.reason,\n issues: bannerIssues(paragraph, options),\n suggestion: null,\n };\n }\n\n if (paragraph.kind === 'tag' && paragraph.reason) {\n const codeTag = CODE_TAGS.has(JSDOC_TAG.exec(first.text)?.[1] ?? '');\n const issues = codeTag ? [] : overflowIssues(paragraph.lines, options);\n\n return { ...base, reflowable: false, reason: paragraph.reason, issues, suggestion: null };\n }\n\n if (paragraph.kind === 'other') {\n const fence = new FenceTracker();\n\n const prose = {\n ...paragraph,\n lines: paragraph.lines.filter((line) => !fence.next(stripBlockSyntax(line.text))),\n };\n\n const structure = structureOf(\n prose.lines.map((line) => {\n return { ...line, text: stripBlockSyntax(line.text) };\n }),\n );\n\n const issues = keptIssues(prose, structure, options);\n\n return { ...base, reflowable: false, reason: paragraph.reason, issues, suggestion: null };\n }\n\n const keep = (structure: Structure): ParagraphReport => {\n const issues = keptIssues(paragraph, structure, options);\n\n return { ...base, reflowable: false, reason: structure.reason, issues, suggestion: null };\n };\n\n const structure = structureOf(paragraph.lines);\n\n if (structure) {\n return keep(structure);\n }\n\n const prefix = first.prefix;\n const prefixLength = visualLength(prefix, options.tabWidth);\n const hang = ' '.repeat(paragraph.marker.length);\n const firstLimit = options.width - prefixLength - paragraph.marker.length;\n const restLimit = options.width - prefixLength - hang.length;\n\n const currentLines = paragraph.lines.map((line, position) =>\n bindLineStarts(tokenize(position === 0 ? line.text.slice(paragraph.marker.length) : line.text), startsLine),\n );\n\n const tokens = tokenize(currentLines.flat().join(' '));\n\n const layoutOptions = {\n ...options,\n tolerance:\n options.maxWidth === undefined\n ? options.tolerance\n : Math.min(options.tolerance, options.maxWidth - options.width),\n parentheticalOverflow: options.maxWidth === undefined ? undefined : options.maxWidth - options.width,\n keepFirstLine: paragraph.kind === 'tag',\n };\n\n const layOut = (from: readonly string[]) => {\n const even = layoutTokens(from, firstLimit, restLimit, layoutOptions);\n\n return { even, wrapped: breakAfterSentence(even, firstLimit, restLimit, layoutOptions) };\n };\n\n let units = bindLineStarts(paragraph.kind === 'tag' ? bindTagHead(tokens) : tokens, startsLine);\n\n // the first and the other lines have the same limit, since the hanging indent is as long as the marker\n const hardLimit = restLimit + (options.maxWidth === undefined ? options.tolerance : options.maxWidth - options.width);\n const written = currentLines.map((words) => ` ${words.join(' ')} `);\n\n /**\n * Text that stays on one line, like a code span, may only be longer than the limit when it\n * already is on one line, so a rewrap never adds a line that is too long.\n */\n const staysInLimit = (unit: string): boolean => {\n return unit.length <= hardLimit || written.some((line) => line.includes(` ${unit} `));\n };\n\n // a code span or `{...}` group that continues on the next line would be joined into one line that is too long\n if (!units.every((unit) => staysInLimit(unit))) {\n return keep({ tier: 'manual', reason: 'unbreakable text across lines' });\n }\n\n let { even, wrapped } = layOut(units);\n\n const bodyOf = (lines: readonly (readonly string[])[]): BodyLine[] => {\n return lines.map((words, position) => {\n const text = `${position === 0 ? paragraph.marker : hang}${words.join(' ')}`;\n\n return { ...first, text, verbatim: false, hardBreak: HARD_BREAK.test(text) };\n });\n };\n\n /**\n * Analyzing the rewrap must find the same prose, so when a new line's start or end would read as\n * structure, like two lines that start with `term - `, the first word of the line that tips it\n * moves up a line. Without such a layout, or when the text reads as structure on a single line,\n * no rewrap reads as the same prose, and the paragraph is kept as written.\n */\n for (;;) {\n const body = bodyOf(wrapped);\n const rewrap = structureOf(body);\n\n if (!rewrap) {\n break;\n }\n\n // the first line has no line to move a word up to, so the first word of the second line joins it instead\n const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1)) !== undefined);\n const start = wrapped.slice(0, Math.max(tipping, 1)).flat().length;\n const pair = `${units[start - 1]} ${units[start]}`;\n\n if (body.length < 2 || !staysInLimit(pair)) {\n const reading = rewrap.tier === 'code-like' ? 'code' : rewrap.reason;\n\n return keep({ tier: 'manual', reason: `rewrap would read as ${reading}` });\n }\n\n units = units.toSpliced(start - 1, 2, pair);\n ({ even, wrapped } = layOut(units));\n }\n\n const matchesLayout = sameLayout(wrapped, currentLines);\n\n const lineIssues: LineIssue[] = [];\n const overflow: IssueReport[] = options.maxWidth === undefined ? [] : overflowIssues(paragraph.lines, options);\n\n if (!matchesLayout) {\n const hasExtraLines = currentLines.length > wrapped.length;\n\n if (options.maxWidth === undefined) {\n overflow.push(...overflowIssues(paragraph.lines, options));\n }\n\n lineIssues.push(\n ...findLineIssues(currentLines, firstLimit, restLimit, layoutOptions).filter(\n (issue) => issue.type !== 'overflow' && (issue.type !== 'underfill' || hasExtraLines),\n ),\n );\n\n if (hasWidow(currentLines) && !hasWidow(wrapped)) {\n lineIssues.push({ type: 'widow', line: currentLines.length - 1 });\n }\n\n /**\n * Lines that only miss the break after a sentence are a hint, since a heuristic decides where\n * a sentence ends.\n */\n if (\n lineIssues.length === 0 &&\n overflow.length === 0 &&\n (currentLines.length === wrapped.length || options.maxWidth !== undefined)\n ) {\n const line = wrapped.findIndex((words, position) => words.join(' ') !== currentLines[position]?.join(' '));\n\n lineIssues.push({ type: sameLayout(even, currentLines) ? 'orphan' : 'unbalanced', line: Math.max(line, 0) });\n }\n }\n\n const suggestion = wrapped.map(\n (words, position) => `${prefix}${position === 0 ? paragraph.marker : hang}${words.join(' ')}`,\n );\n\n const wrapIssues = lineIssues.map((issue) => lineIssue(issue.type, paragraph.lines[issue.line], options));\n const issues = byLine([...overflow, ...wrapIssues, ...styleIssues(paragraph, options)]);\n\n return { ...base, reflowable: true, issues, suggestion };\n}\n\nfunction computeLineStarts(source: string): number[] {\n const starts = [0];\n\n for (let index = 0; index < source.length; index += 1) {\n if (source.codePointAt(index) === 0x0a) {\n starts.push(index + 1);\n }\n }\n\n return starts;\n}\n\nfunction lineOf(lineStarts: readonly number[], offset: number): number {\n let low = 0;\n let high = lineStarts.length - 1;\n\n while (low < high) {\n const middle = Math.ceil((low + high) / 2);\n\n if (lineStarts[middle] <= offset) {\n low = middle;\n } else {\n high = middle - 1;\n }\n }\n\n return low;\n}\n\ninterface CommentShape {\n readonly kind: CommentKind;\n readonly startLine: number;\n readonly endLine: number;\n readonly paragraphs: RawParagraph[];\n}\n\nfunction stripCarriageReturn(line: string): string {\n return line.endsWith('\\r') ? line.slice(0, -1) : line;\n}\n\nfunction shapeBlock(\n comment: ParsedComment,\n sourceLines: readonly string[],\n lineStarts: readonly number[],\n): CommentShape {\n const startLine = lineOf(lineStarts, comment.start);\n const endLine = lineOf(lineStarts, comment.end - 1);\n const kind: CommentKind = comment.value.startsWith('*') ? 'jsdoc' : 'block';\n const lines = sourceLines.slice(startLine, endLine + 1);\n\n const wholeComment = (paragraphKind: ParagraphKind, reason: string): CommentShape => {\n const otherLines = lines.map((text, offset) => {\n return { index: startLine + offset, prefix: '', text, verbatim: false, hardBreak: false };\n });\n\n return { kind, startLine, endLine, paragraphs: [{ kind: paragraphKind, marker: '', reason, lines: otherLines }] };\n };\n\n const value = comment.value.replace(/^\\*/, '');\n\n // a code span only names a marker, like the `@license` in a comment that explains legal comments\n if (LEGAL_COMMENT.test(replaceKeepingLines(comment.value, CODE_SPAN, '``'))) {\n return wholeComment('code', 'legal comment');\n }\n\n if (LINE_DIRECTIVE.test(value) || BLOCK_DIRECTIVE.test(value)) {\n return wholeComment('code', 'directive');\n }\n\n const other = (): CommentShape => {\n return wholeComment(\n 'other',\n startLine === endLine ? 'single-line block comment' : 'unsupported block comment layout',\n );\n };\n\n if (startLine === endLine || !/^\\s*\\/\\*\\*?$/.test(lines[0]) || !/^\\s*\\*\\/$/.test(lines.at(-1) ?? '')) {\n return other();\n }\n\n const body: BodyLine[] = [];\n const inner = lines.slice(1, -1);\n\n for (const [offset, line] of inner.entries()) {\n const match = /^(\\s*\\*)( ?)(.*)$/.exec(line);\n\n if (!match) {\n return other();\n }\n\n const [, star, space, text] = match;\n\n body.push({\n index: startLine + 1 + offset,\n prefix: `${star} `,\n text: text.trimEnd(),\n verbatim: (!space && text.length > 0) || LINE_DIRECTIVE.test(text),\n hardBreak: HARD_BREAK.test(text),\n });\n }\n\n return { kind, startLine, endLine, paragraphs: segment(body) };\n}\n\nfunction ownsLines(comment: ParsedComment, source: string, lineStarts: readonly number[]): boolean {\n const startLine = lineOf(lineStarts, comment.start);\n const before = source.slice(lineStarts[startLine], comment.start);\n const endLine = lineOf(lineStarts, comment.end - 1);\n const lineEnd = lineStarts[endLine + 1] ?? source.length;\n const after = source.slice(comment.end, lineEnd);\n\n return /^[\\t ]*$/.test(before) && /^[\\t ]*\\r?\\n?$/.test(after);\n}\n\n/**\n * Only a single-line block comment or prose without structure can stay on one line past the width.\n */\nfunction singleLineLength(paragraph: RawParagraph, options: AnalyzeOptions): number {\n const [first] = paragraph.lines;\n\n if (paragraph.kind === 'other' && paragraph.reason === 'single-line block comment') {\n return sourceLineLength(first, options.tabWidth);\n }\n\n if (paragraph.kind !== 'prose' || structureOf(paragraph.lines)) {\n return Infinity;\n }\n\n const text = tokenize(paragraph.lines.map((line) => line.text).join(' ')).join(' ');\n\n return visualLength(first.prefix, options.tabWidth) + text.length;\n}\n\nexport const BYTE_ORDER_MARK = '\\uFEFF';\n\n/**\n * Most toolchains accept JSX in a `.js` file, and a file without JSX parses the same either way.\n */\nfunction parseSource(filename: string, source: string): ReturnType<typeof parseSync> {\n return parseSync(filename, source, filename.endsWith('.js') ? { lang: 'jsx' } : undefined);\n}\n\nfunction describeFailure(errors: readonly OxcError[], lineStarts: readonly number[]): SyntaxFailure {\n const [first] = errors;\n const offset = first.labels.at(0)?.start;\n\n return { message: first.message, line: offset === undefined ? undefined : lineOf(lineStarts, offset) + 1 };\n}\n\n/**\n * Finds all own-line block and JSDoc comments in a JavaScript or TypeScript source file and reports\n * how each paragraph deviates from a greedy wrap at `options.width`. Line comments (`//`) are never\n * analyzed. A source with a syntax error that stops the parser has no reports.\n */\nexport function analyzeSource(source: string, filename: string, options: AnalyzeOptions): CommentReport[] {\n return inspectSource(source, filename, options).comments;\n}\n\n/**\n * Analyzes a source like `analyzeSource`, and also tells a source without comments apart from one\n * that the parser can't read.\n */\nexport function inspectSource(source: string, filename: string, options: AnalyzeOptions): SourceAnalysis {\n if (options.maxWidth !== undefined && (!Number.isInteger(options.maxWidth) || options.maxWidth < options.width)) {\n throw new Error('comments: maxWidth must be an integer at least as large as width');\n }\n\n // a byte order mark is no text, so a comment on the first line owns its lines and is measured without the mark\n const text = source.startsWith(BYTE_ORDER_MARK) ? source.slice(1) : source;\n const { program, comments, errors } = parseSource(filename, text);\n const lineStarts = computeLineStarts(text);\n\n /**\n * The parser returns an empty program when it stops at a syntax error, and it has only seen\n * the comments before that error. An error that it recovers from, like a `return` outside of\n * a function, leaves the program and its comments complete.\n */\n if (errors.length > 0 && program.body.length === 0) {\n return { comments: [], syntaxError: describeFailure(errors, lineStarts) };\n }\n\n const sourceLines = text.split('\\n').map((line) => stripCarriageReturn(line));\n\n // drop a leading hashbang, which some parser versions report as a comment\n const parsed = (comments as ParsedComment[]).filter((comment) => !(comment.start === 0 && text.startsWith('#!')));\n const reports: CommentReport[] = [];\n\n const blocks = parsed.filter((comment) => {\n return (\n comment.type === 'Block' && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE)\n );\n });\n\n for (const block of blocks) {\n const shape = shapeBlock(block, sourceLines, lineStarts);\n\n const singleLineWidth = Math.max(\n options.width,\n Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity),\n );\n\n const paragraphOptions =\n shape.paragraphs.length === 1 && singleLineLength(shape.paragraphs[0], options) <= singleLineWidth\n ? { ...options, width: singleLineWidth }\n : options;\n\n const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);\n\n reports.push({\n file: filename,\n kind: shape.kind,\n startLine: shape.startLine + 1,\n endLine: shape.endLine + 1,\n indent: /^[\\t ]*/.exec(lines[0])?.[0] ?? '',\n width: options.width,\n maxWidth: options.maxWidth,\n lengths: lines.map((line) => visualLength(line.trimEnd(), options.tabWidth)),\n text: lines.join('\\n'),\n paragraphs: shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions)),\n });\n }\n\n return { comments: reports };\n}\n\n/**\n * Whether the comment has any issue, including hints, which makes it worth handing to the agent.\n */\nexport function isSuspicious(report: CommentReport): boolean {\n return report.paragraphs.some((paragraph) => paragraph.issues.length > 0);\n}\n\n/**\n * Whether the comment has an issue that isn't a hint, so it breaks the rules no matter what\n * the agent decided.\n */\nexport function hasVerdictIssues(report: CommentReport): boolean {\n return report.paragraphs.some((paragraph) => paragraph.issues.some((issue) => issue.tier === 'verdict'));\n}\n\n/**\n * Returns the comment's source text with every prose paragraph replaced by its suggestion.\n * Code, tables, diagrams, and similar paragraphs are kept as they are.\n */\nexport function rewrapComment(report: CommentReport): string {\n const lines = report.text.split('\\n');\n\n const paragraphs = report.paragraphs\n .filter((paragraph) => paragraph.suggestion !== null)\n .toSorted((a, b) => b.startLine - a.startLine);\n\n for (const paragraph of paragraphs) {\n const start = paragraph.startLine - report.startLine;\n\n lines.splice(start, paragraph.endLine - paragraph.startLine + 1, ...(paragraph.suggestion ?? []));\n }\n\n return lines.join('\\n');\n}\n\nconst POSITION_KEYS = new Set(['start', 'end', 'range', 'loc']);\n\n/**\n * Returns the syntax tree of the source as JSON, without positions or comments, used to verify\n * that only comments changed. Moving or rewording a comment keeps it the same, while any change\n * to a string, template, regex, JSX text, or where a statement ends changes it. A source that\n * doesn't parse has no tree to compare, so all of its text counts.\n */\nexport function codeFingerprint(source: string, filename: string): string {\n const { program, errors } = parseSource(filename, source);\n\n if (errors.length > 0) {\n return JSON.stringify({ source, errors: errors.map((error) => error.message) });\n }\n\n // a BigInt literal's value is a `bigint`, which JSON can't hold, but its `raw` text already tells it apart\n return JSON.stringify(program, (key, value: unknown) =>\n POSITION_KEYS.has(key) || typeof value === 'bigint' ? undefined : value,\n );\n}\n","export type StyleIssueType = 'semicolon' | 'em-dash' | 'dash' | 'label-colon' | 'emphasis' | 'banner';\n\nexport interface StyleLine {\n /**\n * This is the 0-based line in the file.\n */\n readonly index: number;\n readonly text: string;\n}\n\nexport interface StyleIssue {\n readonly type: StyleIssueType;\n readonly index: number;\n}\n\n/**\n * Matches four or more repeated characters at the start or end of a line, since three like `===`,\n * `---`, or `~~~` are operators, rules, and fences in prose. A run in the middle of a line is a\n * table rule like `| ---- |` or an arrow like `---->`, and a Markdown heading like `#### Examples`\n * isn't a banner either.\n */\nconst BANNER = /^(?!#{1,6}\\s)([-=*#_~+/\\\\])\\1{3,}|([-=*#_~])\\2{3,}\\s*$/;\n\n/**\n * Runs of the same character separated only by spaces, like `-------- -----`, underline the\n * columns of a table.\n */\nconst COLUMN_RULE = /^([-=_~])\\1*(?:\\s+\\1+)+$/;\n\n/**\n * Matches an en-dash, a spaced hyphen, or a double hyphen between words, which may stand in for an\n * em-dash. An en-dash or hyphen between numbers, like `10 – 20`, is a range, and a double hyphen\n * next to a CLI flag, like `--watch -- file` or `npm test -- --watch`, separates arguments.\n */\nconst DASH = /(?<!\\d)\\s[–-]\\s(?!\\d)|(?<!(?:^|\\s)-[\\w-]*)\\s--\\s(?!-)/;\nconst SEMICOLON = /;(?:\\s|$)/;\n\n/**\n * Matches `*emphasis*` or `**bold**`, which starts with a letter or digit and whose\n * closing asterisks end the word, so that globs like `*foo*.ts` or `*.test.*` aren't\n * emphasis. Asterisks in quotes, like `'*foo*'`, are a string or glob.\n */\nconst EMPHASIS = /(?:^|[\\s(])(\\*\\*?)(?=[\\p{L}\\p{N}])[^*]+?(?<!\\s)\\1(?=$|[\\s)\"']|[.,;:!?](?:$|[\\s)\"']))/u;\n\nexport const LIST_MARKER = /^(?:[-*+]|\\d+[.)])\\s+/;\n\n/**\n * A code span, as in Markdown, closes with a run of as many backticks as it opened with,\n * like ``` `` `a` `` ```, and may continue on the next line.\n */\nexport const CODE_SPAN = /(?<!`)(`+)(?!`)[\\s\\S]*?(?<!`)\\1(?!`)/g;\n\n/**\n * Matches a colon after a word with more text after it on the same line, like `Optional: skips it`\n * or `...through its presets: the rules that...`. A colon that ends a line introduces what's below\n * it. Markers like `TODO:` or `WARNING:` that start a sentence are conventions read by tooling,\n * and a colon after a code span or `{@link}` (replaced by `code` in prose) introduces a definition,\n * like `` `width`: the maximum width ``, so neither counts. Neither does an identifier in camel or\n * snake case, like `maxRetries: 3`, which is a property.\n */\nconst COLON =\n /(?<=[\\w)\\]'\"’])(?<!(?:^|[.!?]\\s+)(?:[A-Z]{2,}|[Tt]odo|[Ff]ixme|[Hh]ack|[Xx]xx))(?<!\\b(?:code|url|entity|[A-Za-z][a-z\\d]*[A-Z]\\w*|\\w*_\\w*)):\\s+\\S/;\n\nexport function replaceKeepingLines(text: string, pattern: RegExp, replacement: string): string {\n return text.replaceAll(pattern, (match) => `${replacement}${'\\n'.repeat(match.split('\\n').length - 1)}`);\n}\n\nexport function stripNonProse(text: string): string {\n let stripped = replaceKeepingLines(text, CODE_SPAN, 'code');\n\n for (let previous = ''; previous !== stripped; ) {\n previous = stripped;\n stripped = replaceKeepingLines(stripped, /\\{[^{}]*\\}/g, 'code');\n }\n\n return stripped\n .replaceAll(/\\b[a-z][\\w+.-]*:\\/\\/\\S+/gi, 'url')\n .replaceAll(/&(?:[a-z][a-z\\d]*|#\\d+|#x[\\da-f]+);/gi, 'entity');\n}\n\nfunction hasExplanatoryColon(text: string): boolean {\n return COLON.test(text.replace(LIST_MARKER, ''));\n}\n\nexport function isBanner(text: string): boolean {\n const prose = stripNonProse(text.trim());\n\n return BANNER.test(prose) && !COLUMN_RULE.test(prose);\n}\n\nexport function findStyleIssues(lines: readonly StyleLine[]): StyleIssue[] {\n const issues: StyleIssue[] = [];\n const prose = stripNonProse(lines.map((line) => line.text.trim()).join('\\n'));\n\n /**\n * A JSDoc tag's description starts a sentence after its `@param name -` style head.\n * The head, including its type, may wrap across lines.\n */\n const proseLines = replaceKeepingLines(prose, /^@\\w+\\s+(?:code\\s+)?(?:\\S+\\s+-\\s+)?/gm, '').split('\\n');\n\n for (const [position, line] of lines.entries()) {\n const text = proseLines[position].trim();\n\n const report = (type: StyleIssueType): void => {\n issues.push({ type, index: line.index });\n };\n\n if (isBanner(line.text)) {\n report('banner');\n } else {\n if (SEMICOLON.test(text)) {\n report('semicolon');\n }\n\n if (text.includes('—')) {\n report('em-dash');\n }\n\n if (DASH.test(text)) {\n report('dash');\n }\n\n if (hasExplanatoryColon(text)) {\n report('label-colon');\n }\n\n if (EMPHASIS.test(text)) {\n report('emphasis');\n }\n }\n }\n\n return issues;\n}\n","export interface WrapOptions {\n /**\n * Sets whether a line may not end with the first word of a sentence, clause, or parenthetical,\n * like `This`, `the` after a comma, or `(the`. Such a word moves to the next line instead.\n * It also sets whether the first of two lines ends with a sentence instead of the start of the\n * next one, as long as the second line is then no longer than the first.\n */\n readonly avoidSentenceOrphans: boolean;\n\n /**\n * Sets how many columns a line may exceed its limit before it counts as overflowing.\n */\n readonly tolerance: number;\n\n /**\n * Sets how many extra columns a complete parenthetical may use beyond the preferred line limit.\n */\n readonly parentheticalOverflow?: number;\n}\n\nexport type LineIssueType = 'overflow' | 'underfill' | 'orphan' | 'widow' | 'unbalanced';\n\nexport interface LineIssue {\n readonly type: LineIssueType;\n\n /**\n * Holds the index of the offending line within the paragraph.\n */\n readonly line: number;\n}\n\n// punctuation inside a code span, like `,` or `?.`, is code and doesn't end a clause or sentence\nconst CLAUSE_END = /[.!?,;:][\"')\\]*_]*$/;\nconst SENTENCE_END = /[.!?][\"')\\]*_]*$/;\n\n/**\n * Holds only abbreviations that never end a sentence, unlike `no.` or `max.`, which can also be the\n * last word of one.\n */\nconst ABBREVIATIONS = new Set([\n 'e.g.',\n 'i.e.',\n 'etc.',\n 'vs.',\n 'cf.',\n 'approx.',\n 'incl.',\n 'resp.',\n 'al.',\n 'viz.',\n 'esp.',\n 'ca.',\n 'fig.',\n 'figs.',\n 'eq.',\n 'vol.',\n 'pp.',\n 'mr.',\n 'mrs.',\n 'ms.',\n 'dr.',\n 'prof.',\n 'st.',\n]);\n\nexport function isAbbreviation(word: string): boolean {\n const bare = word\n .replace(/^[([\"']+/, '')\n .replace(/[)\\]\"']+$/, '')\n .toLowerCase();\n\n return ABBREVIATIONS.has(bare) || /^(?:[a-z]\\.){2,}$/.test(bare);\n}\n\nfunction findClosingBrace(text: string, start: number): number {\n let depth = 0;\n\n for (let index = start; index < text.length; index += 1) {\n if (text[index] === '{') {\n depth += 1;\n } else if (text[index] === '}') {\n depth -= 1;\n\n if (depth === 0) {\n return index;\n }\n }\n }\n\n return -1;\n}\n\nfunction backtickRun(text: string, start: number): number {\n let end = start;\n\n while (text[end] === '`') {\n end += 1;\n }\n\n return end - start;\n}\n\n/**\n * As in Markdown, a code span opened by a run of backticks only closes with a run of the same\n * length, like the two in ``` `` `a` `` ```.\n */\nfunction findCodeSpanEnd(text: string, start: number): number {\n const run = backtickRun(text, start);\n\n for (let index = text.indexOf('`', start + run); index !== -1; ) {\n const length = backtickRun(text, index);\n\n if (length === run) {\n return index + length - 1;\n }\n\n index = text.indexOf('`', index + length);\n }\n\n return -1;\n}\n\n/**\n * JSDoc writes types and `{@link ...}` tags as `{...}` groups, so they stay intact like code spans.\n */\nexport function tokenize(text: string): string[] {\n const tokens: string[] = [];\n\n let current = '';\n let index = 0;\n\n while (index < text.length) {\n const char = text[index];\n const close = char === '`' ? findCodeSpanEnd(text, index) : char === '{' ? findClosingBrace(text, index) : -1;\n\n // a run of backticks that never closes is literal text, and none of its backticks open a span\n if (char === '`' && close === -1) {\n const run = backtickRun(text, index);\n\n current += text.slice(index, index + run);\n index += run;\n\n continue;\n }\n\n if (close !== -1) {\n current += text.slice(index, close + 1);\n index = close + 1;\n\n continue;\n }\n\n if (/\\s/.test(char)) {\n if (current) {\n tokens.push(current);\n\n current = '';\n }\n\n index += 1;\n\n continue;\n }\n\n current += char;\n index += 1;\n }\n\n if (current) {\n tokens.push(current);\n }\n\n return tokens;\n}\n\nfunction parentheticalEnd(text: string): number {\n let depth = 0;\n\n for (let index = 0; index < text.length; index += 1) {\n const char = text[index];\n\n if (char === '\\\\') {\n index += 1;\n\n continue;\n }\n\n const close = char === '`' ? findCodeSpanEnd(text, index) : char === '{' ? findClosingBrace(text, index) : -1;\n\n if (close !== -1) {\n index = close;\n } else if (char === '(') {\n depth += 1;\n } else if (char === ')' && --depth === 0) {\n return index;\n }\n }\n\n return -1;\n}\n\nfunction isParenthetical(token: string): boolean {\n return token.startsWith('(') && parentheticalEnd(token) !== -1;\n}\n\ninterface ParentheticalGroup {\n readonly start: number;\n readonly end: number;\n}\n\nfunction parentheticalGroups(tokens: readonly string[], limit: number): ParentheticalGroup[] {\n const groups: ParentheticalGroup[] = [];\n\n for (let index = 0; index < tokens.length; index += 1) {\n if (tokens[index].startsWith('(')) {\n const end = parentheticalEnd(tokens.slice(index).join(' '));\n\n let last = index;\n let length = tokens[index].length;\n\n while (length <= end && last + 1 < tokens.length) {\n length += 1 + tokens[++last].length;\n }\n\n if (end !== -1 && length <= limit) {\n groups.push({ start: index, end: last + 1 });\n\n index = last;\n\n continue;\n }\n }\n }\n\n return groups;\n}\n\nexport function bindParentheticals(tokens: readonly string[], limit: number): string[] {\n const groups = new Map(parentheticalGroups(tokens, limit).map((group) => [group.start, group.end]));\n const units: string[] = [];\n\n for (let index = 0; index < tokens.length; index += 1) {\n const end = groups.get(index) ?? index + 1;\n\n units.push(tokens.slice(index, end).join(' '));\n\n index = end - 1;\n }\n\n return units;\n}\n\nfunction endsClause(token: string): boolean {\n return CLAUSE_END.test(token) && !isAbbreviation(token);\n}\n\nfunction endsSentence(token: string): boolean {\n return SENTENCE_END.test(token) && !isAbbreviation(token);\n}\n\nfunction opensAside(token: string): boolean {\n return /^[([]/.test(token) && !/[)\\]][\"'`.,;:!?]*$/.test(token);\n}\n\nfunction isOrphan(previous: string, token: string): boolean {\n return opensAside(token) || (endsClause(previous) && !CLAUSE_END.test(token));\n}\n\n/**\n * Returns whether Markdown or JSDoc reads a word as the start of a block when it begins a line,\n * like a list marker (`-`, `1.`), a heading (`#`), a quote (`>`), a fence, a table row, an HTML\n * tag, or a JSDoc tag like `@param`. Wrapping must never move such a word to the start of a line,\n * or the paragraph would turn into something else.\n */\nexport function startsBlock(token: string): boolean {\n return /^(?:[-*+>]|\\d+[.)]|#{1,6})$/.test(token) || /^(?:@[A-Za-z]|`{3}|~{3}|\\||<\\/?[A-Za-z])/.test(token);\n}\n\nexport function bindLineStarts(\n tokens: readonly string[],\n startsLine: (token: string, next: string | undefined) => boolean = startsBlock,\n): string[] {\n const units: string[] = [];\n\n for (const [index, token] of tokens.entries()) {\n if (units.length > 0 && startsLine(token, tokens[index + 1])) {\n units[units.length - 1] += ` ${token}`;\n } else {\n units.push(token);\n }\n }\n\n return units;\n}\n\nfunction breaksBefore(previous: string, last: string, next: string, options: WrapOptions): boolean {\n return (options.avoidSentenceOrphans && isOrphan(previous, last)) || startsBlock(next);\n}\n\nfunction lineLength(tokens: readonly string[]): number {\n let length = 0;\n\n for (const token of tokens) {\n length += token.length;\n }\n\n return length + Math.max(0, tokens.length - 1);\n}\n\n/**\n * The first line can have a different limit than the rest, e.g. when a list marker takes up space\n * only on the first line.\n */\nexport function wrapTokens(\n tokens: readonly string[],\n firstLimit: number,\n restLimit: number,\n options: WrapOptions,\n): string[][] {\n return wrapFrom(tokens, 0, firstLimit, restLimit, options, Infinity);\n}\n\n/**\n * Wraps the tokens from the one at `from` on, and stops after `maxLines` lines. A line\n * never depends on the lines after it, so the first lines of a wrap are the same with or\n * without the rest.\n */\nfunction wrapFrom(\n tokens: readonly string[],\n from: number,\n firstLimit: number,\n restLimit: number,\n options: WrapOptions,\n maxLines: number,\n): string[][] {\n const lines: string[][] = [];\n\n let line: string[] = [];\n let length = 0;\n\n for (let index = from; index < tokens.length; index += 1) {\n const token = tokens[index];\n\n while (line.length > 0) {\n const limit = lines.length === 0 ? firstLimit : restLimit;\n const nextLength = length + 1 + token.length;\n const extra = isParenthetical(token) ? (options.parentheticalOverflow ?? 0) : 0;\n\n if (nextLength <= limit + extra) {\n break;\n }\n\n const carry: string[] = [];\n\n while (line.length >= 2 && breaksBefore(line.at(-2) ?? '', line.at(-1) ?? '', carry[0] ?? token, options)) {\n carry.unshift(line.pop() ?? '');\n }\n\n lines.push(line);\n\n if (lines.length >= maxLines) {\n return lines;\n }\n\n line = carry;\n length = lineLength(carry);\n }\n\n line.push(token);\n\n length = lineLength(line);\n }\n\n if (line.length > 0) {\n lines.push(line);\n }\n\n return lines;\n}\n\nconst MIN_LAST_LINE_WORDS = 3;\n\nexport function hasWidow(lines: readonly (readonly string[])[]): boolean {\n const last = lines.at(-1) ?? [];\n const words = last.reduce((count, token) => count + (isParenthetical(token) ? tokenize(token).length : 1), 0);\n\n return lines.length >= 2 && words < MIN_LAST_LINE_WORDS;\n}\n\nexport interface LayoutOptions extends WrapOptions {\n /**\n * Keeps the first line out of the balancing, e.g. for a JSDoc tag whose first\n * line holds `@param {Type} name -`.\n */\n readonly keepFirstLine?: boolean;\n}\n\n/**\n * Sets how heavily lines that come close to the width count against lines that are even. With too\n * much weight, lines of 86 and 89 columns beat two of 86 at a width of 100, which leaves a word\n * like `as` at the end of the longer line, away from the words that it belongs to.\n */\nconst FULLNESS_WEIGHT = 0.07;\nconst PARENTHETICAL_SPLIT_WEIGHT = 2;\n\n/**\n * Holds what the cost of a line depends on, besides the length that the lines aim for.\n */\ninterface BalanceInput {\n /**\n * Holds the column that each token starts at when all tokens share a line, and the column after\n * the last token.\n */\n readonly starts: readonly number[];\n readonly count: number;\n\n /**\n * Holds how many fewer columns the first line has than the other lines.\n */\n readonly offset: number;\n\n /**\n * Holds for each token whether it may not start a line.\n */\n readonly blockStarts: readonly boolean[];\n\n /**\n * Holds for each token whether a line of two or more tokens may not end before it.\n */\n readonly afterOrphan: readonly boolean[];\n readonly splitCosts: ReadonlyMap<number, number>;\n}\n\n/**\n * Holds the cheapest layouts that end one line, one for each token that the line can end before.\n */\ninterface LineCosts {\n /**\n * Holds the token that the first entry of `costs` and `origins` ends before.\n */\n readonly base: number;\n readonly costs: readonly number[];\n\n /**\n * Holds the token that the line starts with in the layout of each entry of `costs`.\n */\n readonly origins: readonly number[];\n\n /**\n * Holds the first token that the line can end before.\n */\n readonly low: number;\n\n /**\n * Holds the last token that the line can end before.\n */\n readonly high: number;\n}\n\n/**\n * Returns, for each token, the fewest lines of at most `target` columns that hold the tokens from\n * it on. No token is longer than `target`.\n */\nfunction fewestLines(starts: readonly number[], target: number): Int32Array {\n const total = starts.length - 1;\n const fewest = new Int32Array(total + 1);\n\n let end = total;\n\n for (let index = total - 1; index >= 0; index -= 1) {\n while (starts[end] - starts[index] - 1 > target) {\n end -= 1;\n }\n\n fewest[index] = fewest[end] + 1;\n }\n\n return fewest;\n}\n\n/**\n * Returns the first index from `from` on whose value is at most `limit`. The values never grow as\n * the index rises, so a binary search can find it.\n */\nfunction firstAtMost(values: Int32Array, limit: number, from: number): number {\n let low = from;\n let high = values.length - 1;\n\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n\n if (values[middle] <= limit) {\n high = middle;\n } else {\n low = middle + 1;\n }\n }\n\n return low;\n}\n\n/**\n * Returns the cheapest layouts that end the given line, or `undefined` when the line can't end\n * anywhere. It only looks at the ends that a whole layout can use. The lines before it reach up\n * to an end, and the lines after it can hold the rest from an end on, which leaves a narrow band\n * of tokens instead of all of them.\n */\nfunction costsOfLine(\n input: BalanceInput,\n target: number,\n line: number,\n previous: LineCosts,\n fewest: Int32Array,\n): LineCosts | undefined {\n const { starts, afterOrphan } = input;\n const offset = line === 1 ? input.offset : 0;\n const linesLeft = input.count - line;\n\n // each line after this one needs a token\n const last = starts.length - 1 - linesLeft;\n const base = firstAtMost(fewest, linesLeft, Math.max(line, previous.low + 1));\n const costs: number[] = [];\n const origins: number[] = [];\n\n let low = -1;\n let high = -1;\n\n for (let end = base; end <= last; end += 1) {\n const nearest = Math.min(end - 1, previous.high);\n\n // from here on, a line is too long even when it starts at the last end of the line before it\n if (end > previous.high && starts[end] - starts[nearest] - 1 + offset > target) {\n break;\n }\n\n let cost = Infinity;\n let origin = -1;\n\n if (!input.blockStarts[end]) {\n const splitCost = input.splitCosts.get(end) ?? 0;\n\n for (let start = nearest; start >= previous.low; start -= 1) {\n const length = starts[end] - starts[start] - 1 + offset;\n\n if (length > target) {\n break;\n }\n\n const before = previous.costs[start - previous.base];\n\n if (before === Infinity || (afterOrphan[end] && end - start >= 2)) {\n continue;\n }\n\n const candidate = before + (target - length) ** 2 + splitCost;\n\n if (candidate <= cost) {\n cost = candidate;\n origin = start;\n }\n }\n }\n\n costs.push(cost);\n origins.push(origin);\n\n if (cost !== Infinity) {\n low = low === -1 ? end : low;\n high = end;\n }\n }\n\n return low === -1 ? undefined : { base, costs, origins, low, high };\n}\n\n/**\n * Lays the tokens out in `count` lines that are as even as possible. It tries every length that\n * the lines can aim for and keeps the layout with the lowest cost. Of two layouts with the same\n * cost, it keeps the one that aims for longer lines, and then the one with the longer last line.\n */\nfunction balanceTokens(\n tokens: readonly string[],\n firstLimit: number,\n restLimit: number,\n count: number,\n options: WrapOptions,\n preferredLimit = restLimit,\n splitCosts: ReadonlyMap<number, number> = new Map(),\n): string[][] | undefined {\n const total = tokens.length;\n const starts = [0];\n\n let longest = -Infinity;\n\n for (const token of tokens) {\n starts.push((starts.at(-1) ?? 0) + token.length + 1);\n\n longest = Math.max(longest, token.length);\n }\n\n const input: BalanceInput = {\n starts,\n count,\n offset: restLimit - firstLimit,\n blockStarts: tokens.map((token) => startsBlock(token)),\n afterOrphan: tokens.map((_, index) => {\n return options.avoidSentenceOrphans && index >= 2 && isOrphan(tokens[index - 2], tokens[index - 1]);\n }),\n splitCosts,\n };\n\n const targets: number[] = [];\n\n for (let target = restLimit; target >= Math.max(longest, Math.floor(restLimit / 2)); target -= 1) {\n targets.push(target);\n }\n\n // the lines before the last one hold at most this many columns together, which is when the last line is empty\n const most = starts[total] - count + input.offset;\n\n let best: { cost: number; target: number; breaks: number[] } | undefined;\n\n /**\n * Short targets come first. They rule most layouts out, so they are cheap, and their cost lets\n * the loop skip the long targets, whose lines can't all be full.\n */\n for (const target of targets.toReversed()) {\n const fullness = FULLNESS_WEIGHT * (count - 1) * (preferredLimit - target) ** 2;\n const spare = (count - 1) * target - most;\n\n // lines that are `spare` columns short together cost the least when they are all equally short\n const least = count > 1 && spare > 0 ? Math.max(spare ** 2 / (count - 1) - 1, 0) : 0;\n\n if (best !== undefined && least + fullness > best.cost) {\n continue;\n }\n\n const fewest = fewestLines(starts, target);\n const rows: LineCosts[] = [{ base: 0, costs: [0], origins: [-1], low: 0, high: 0 }];\n\n for (let line = 1; line < count; line += 1) {\n const row = costsOfLine(input, target, line, rows[line - 1], fewest);\n\n if (!row) {\n break;\n }\n\n rows.push(row);\n }\n\n if (rows.length < count) {\n continue;\n }\n\n const row = rows[count - 1];\n\n for (let start = row.low; start <= row.high; start += 1) {\n const score = row.costs[start - row.base] + fullness;\n\n const improves = score < (best?.cost ?? Infinity) || (score === best?.cost && target > best.target);\n\n if (!improves || starts[total] - starts[start] - 1 > target || hasWidow([[], tokens.slice(start)])) {\n continue;\n }\n\n const breaks = [start];\n\n for (let line = count - 1; line > 1; line -= 1) {\n breaks.unshift(rows[line].origins[breaks[0] - rows[line].base]);\n }\n\n best = { cost: score, target, breaks };\n }\n }\n\n if (!best) {\n return undefined;\n }\n\n const bounds = [0, ...best.breaks, total];\n\n return bounds.slice(0, -1).map((start, index) => tokens.slice(start, bounds[index + 1]));\n}\n\nexport function layoutTokens(\n tokens: readonly string[],\n firstLimit: number,\n restLimit: number,\n options: LayoutOptions,\n): string[][] {\n if (options.parentheticalOverflow !== undefined) {\n const grouped = bindParentheticals(tokens, restLimit + options.parentheticalOverflow);\n\n if (grouped.some((token) => isParenthetical(token))) {\n const minimumLines = Math.max(\n 2,\n Math.ceil((lineLength(tokens) + 1 + restLimit - firstLimit) / Math.max(1, restLimit + 1)),\n );\n\n let preserved = wrapTokens(grouped, firstLimit, restLimit, options);\n\n if (minimumLines === 2 && preserved.length > 2) {\n const wide = wrapTokens(\n grouped,\n firstLimit + options.parentheticalOverflow,\n restLimit + options.parentheticalOverflow,\n { ...options, parentheticalOverflow: undefined },\n );\n\n if (wide.length === 2 && !hasWidow(wide)) {\n preserved = wide;\n }\n }\n\n if (preserved.length <= 2 && !hasWidow(preserved)) {\n let cursor = 0;\n\n return preserved.map((line) => {\n const expanded: string[] = [];\n const length = lineLength(line);\n\n let used = -1;\n\n while (cursor < tokens.length && used < length) {\n const token = tokens[cursor++];\n\n expanded.push(token);\n\n used += token.length + 1;\n }\n\n return expanded;\n });\n }\n\n const splitCosts = new Map<number, number>();\n const groups = parentheticalGroups(tokens, restLimit + options.parentheticalOverflow);\n\n for (const group of groups) {\n const length = lineLength(tokens.slice(group.start, group.end));\n\n for (let at = group.start + 1; at < group.end; at += 1) {\n splitCosts.set(at, length * PARENTHETICAL_SPLIT_WEIGHT);\n }\n }\n\n const preferred = wrapTokens(tokens, firstLimit, restLimit, { ...options, parentheticalOverflow: undefined });\n\n for (let count = minimumLines; count <= Math.max(preferred.length + 1, preserved.length); count += 1) {\n const balanced = balanceTokens(\n tokens,\n firstLimit + options.parentheticalOverflow,\n restLimit + options.parentheticalOverflow,\n count,\n options,\n restLimit,\n splitCosts,\n );\n\n if (balanced) {\n return balanced;\n }\n }\n }\n }\n\n const greedy = wrapTokens(tokens, firstLimit, restLimit, options);\n\n if (greedy.length < 2) {\n return greedy;\n }\n\n if (options.keepFirstLine) {\n const [head] = greedy;\n const rest = layoutTokens(tokens.slice(head.length), restLimit, restLimit, { ...options, keepFirstLine: false });\n const lines = [head, ...rest];\n\n if (!hasWidow(lines)) {\n return lines;\n }\n }\n\n const balanced = balanceTokens(tokens, firstLimit, restLimit, greedy.length, options);\n\n if (balanced) {\n return balanced;\n }\n\n const stretched = wrapTokens(tokens, firstLimit + options.tolerance, restLimit + options.tolerance, options);\n\n return hasWidow(stretched) ? greedy : stretched;\n}\n\n/**\n * Ends the first of two lines with a sentence when the rest fits on the second line without\n * sticking out past the first. A second line that would stick out keeps the lines as they are.\n */\nexport function breakAfterSentence(\n lines: string[][],\n firstLimit: number,\n restLimit: number,\n options: WrapOptions,\n): string[][] {\n if (lines.length !== 2 || !options.avoidSentenceOrphans) {\n return lines;\n }\n\n const tokens = lines.flat();\n\n // the first line starts this many columns after the second, so it ends that much later too\n const offset = restLimit - firstLimit;\n\n for (let end = tokens.length - 1; end > 0; end -= 1) {\n const first = tokens.slice(0, end);\n const second = tokens.slice(end);\n\n // a second line that is no longer than a first line within its limit is within its own limit too\n if (\n endsSentence(tokens[end - 1]) &&\n !hasWidow([first, second]) &&\n !breaksBefore(tokens[end - 2] ?? '', tokens[end - 1], tokens[end], options) &&\n lineLength(first) <= firstLimit &&\n lineLength(second) <= lineLength(first) + offset\n ) {\n return [first, second];\n }\n }\n\n return lines;\n}\n\n/**\n * A wrap produced by `wrapTokens` with the same arguments never has issues, unless a single token\n * is wider than the limit.\n */\nexport function findLineIssues(\n lines: readonly (readonly string[])[],\n firstLimit: number,\n restLimit: number,\n options: WrapOptions,\n): LineIssue[] {\n const issues: LineIssue[] = [];\n const tokens = lines.flat();\n\n let next = 0;\n\n for (const [index, line] of lines.entries()) {\n const limit = index === 0 ? firstLimit : restLimit;\n const length = lineLength(line);\n const start = next;\n\n next += line.length;\n\n if (line.length > 1 && length > limit + options.tolerance) {\n issues.push({ type: 'overflow', line: index });\n }\n\n if (index === lines.length - 1 || line.length === 0) {\n continue;\n }\n\n if (options.avoidSentenceOrphans && line.length >= 2 && isOrphan(line.at(-2) ?? '', line.at(-1) ?? '')) {\n issues.push({ type: 'orphan', line: index });\n }\n\n /**\n * Only the first line of a wrap of the rest counts. A line that may end with the start of a\n * sentence is measured too, because the words that fit on it don't depend on its last word.\n */\n const [expected] = wrapFrom(tokens, start, limit, restLimit, options, 1);\n\n if (expected.length > line.length) {\n issues.push({ type: 'underfill', line: index });\n }\n }\n\n return issues;\n}\n","import { spawn } from 'node:child_process';\nimport { readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport type { CommentReport, IssueReport, IssueType, ParagraphReport } from './analyze.js';\nimport type { AgentConfig, AgentRunner } from './config.js';\n\n/**\n * This variable is set in the agent's environment so that the agent's own stop hook doesn't\n * spawn another agent.\n */\nexport const CHILD_ENV = 'COMMENTS_HOOK_CHILD';\n\nexport const DEFAULT_RULES = `\n# Comment rules\n\n## Priorities\n\nWhen rules pull in different directions, the earlier one wins.\n\n1. Keep code and exact text byte for byte, including every punctuation character in it. That covers code blocks, \\`@example\\`, code spans, identifiers, string literals like error messages, regexes, globs and other patterns like \\`*test*\\`, URLs, quoted text, titles, and notation like \\`host:port\\`.\n2. Keep the meaning. An edit may make a comment clearer, never different.\n3. Follow the style preferences below.\n4. Change as little as possible. Wording that already reads clearly stays, even when a note points at it.\n\n## Layout\n\n- After you finish, the tool rewraps prose paragraphs, list items, and JSDoc tag descriptions. The width is a preferred target. A parenthetical may extend a line up to the configured maximum so it stays together. In longer paragraphs, balanced lines take priority over keeping every parenthetical on one line. The tool also avoids short last lines where possible. Leave these line breaks to the tool, except for the sentence-orphan case below.\n- Don't end a line with the first word of a new sentence, clause, or parenthetical, like \\`This\\` after a period, \\`the\\` after a comma, or \\`(the\\`. Move it to the next line instead.\n- In a paragraph of two lines, the first line ends with a sentence whenever the rest fits on the second line and is no longer than the first. Don't end it with the start of the next sentence, like \\`A wrap\\` in \\`...lay out differently. A wrap\\`. Move those words to the second line instead.\n- A comment whose whole text is one paragraph stays on a single line when that line fits a separate, larger single-line width, even past the width. Only a longer comment wraps to the width.\n- Separate paragraphs with an empty comment line.\n- Never split a backtick code span across lines.\n- Keep code blocks, \\`@example\\`, lists, tables, box drawings, aligned columns, and Markdown hard line breaks (a line that ends with two spaces or a backslash) laid out as they are. List items wrap with their continuation lines indented to the item text, and a JSDoc tag description that doesn't fit continues on the next line without indentation.\n- The tool measures every line, taking the tolerance and the parts it keeps as written into account, and the notes on each comment list what is still wrong. Act on those notes. Don't count columns, and don't apply a stricter limit than the notes show.\n- The maximum is a hard limit for measured prose, including long tokens. If a URL, code span, or other exact text exceeds it, preserve that text and explain the unresolved overflow instead of changing its contents.\n- The tool never rewraps the parts it keeps as written, like a single-line block comment, a paragraph with hard line breaks, or lines that may be laid out by hand. When a note says one of their prose lines is too long, break or shorten it yourself. Exact text and intentional layout win over the width, so keep code, code spans, and the layout in them intact, and if a note can only be fixed by changing them, leave it and say why in your reply.\n\n## Style\n\nThese are preferences for comment prose. They never apply to code or exact text (see the priorities above), and a punctuation character on its own is never a reason to edit prose that already reads clearly.\n\n- Write clear sentences that flow. Descriptions read best as complete sentences with a subject and a verb, so prefer \"Returns the bytes the guest sent\" over a fragment like \\`Bytes from the guest.\\` or \\`Optional.\\` Imperative or third-person verb phrases like \"Resolve a hostname...\" or \"Aborts every channel...\" are fine, since the preference is against verb-less fragments, not against concise sentences.\n- Prefer plain sentence punctuation. Where a semicolon, an em-dash (—), or a spaced hyphen, en-dash, or double hyphen standing in for one only joins two thoughts, a period, a comma, parentheses, or a word like \"and\", \"so\", or \"because\" usually reads better. Keep the punctuation where it carries meaning or reads more clearly than the alternatives.\n- Prefer a sentence over a \"Label: explanation\" fragment, like \\`...the socket. Optional: without it...\\` or \\`Default: refuse\\` in the middle of prose. A colon is fine where it introduces a list, code block, or example, in a term's definition, in notation, in a quotation, and in a \\`Note:\\` or \\`Optional:\\` that starts a comment. A JSDoc tag like \\`@note\\` or \\`@todo\\` can replace a label, and \\`@default\\` fits a default value, not a description.\n- Avoid asterisk emphasis like \\`*this*\\`, and use plain wording instead. A name or pattern in asterisks, like \\`*test*\\` or \\`*.spec.ts\\`, is a pattern, so keep its asterisks and wrap it in a code span, like \\`\\` \\`*test*\\` \\`\\`.\n- Avoid section banner comments, i.e. dividers or headings made of dashes, equals signs, asterisks, or other repeated characters that split a file into visual sections. Let the code structure carry the grouping, or write one short, plain sentence comment (e.g. \\`// handlers for control mode\\`). Rules that belong to a diagram, a table, a box drawing, a Markdown document, an example, or a legal header aren't banners.\n- Block and JSDoc comments are capitalized and end with punctuation.\n- Never turn a block or JSDoc comment into \\`//\\` line comments, and never write a run of consecutive \\`//\\` lines.\n\n## Editing\n\n- Keep the wording unless it goes against a rule above. Then reword as little as needed and keep the meaning.\n- Don't reword anything else unless the instructions below allow it.\n`.trim();\n\nconst DEFAULT_TIMEOUT_SECONDS = 300;\nconst DEFAULT_CLAUDE_MODEL = 'haiku';\n\n/**\n * Sets the longest time an agent gets to exit after its timeout before it is killed.\n */\nconst KILL_GRACE_MS = 5000;\n\n/**\n * Sets how long to wait for the rest of the output after the agent exited. A process that the agent\n * left behind keeps the pipes open for as long as it runs.\n */\nconst DRAIN_GRACE_MS = 1000;\n\nconst RUNNER_COMMANDS: Record<AgentRunner, string> = {\n claude: 'claude',\n codex: 'codex',\n cursor: 'cursor-agent',\n};\n\nexport interface AgentInvocation {\n readonly command: string;\n readonly args: readonly string[];\n readonly stdin?: string;\n}\n\nexport interface AgentResult {\n readonly ok: boolean;\n readonly output: string;\n readonly error?: string;\n}\n\n// hints are phrased as possibilities, since they come from heuristics that the agent may reject\nconst ISSUE_DESCRIPTIONS: Record<Exclude<IssueType, 'overflow'>, string> = {\n underfill: 'ends early, and words from the next line fit on it',\n widow: 'leaves fewer than three words on the last line of its paragraph',\n unbalanced: 'is in a paragraph whose lines are less even than the suggested rewrap',\n orphan:\n 'may end with the start of a new sentence, clause, or parenthetical, which belongs on the next line (leave it if the period or comma before it is part of an abbreviation, a name, or notation like `No. 3`)',\n 'em-dash':\n \"may use an em-dash to join two thoughts (leave it if it's in a quotation, a title, or a string literal, or reads more clearly than the alternatives)\",\n banner:\n \"may be a section banner (leave it if it's part of a diagram, a table, a box, a Markdown rule, a separator in an example, or legal text)\",\n semicolon:\n \"may join clauses with a semicolon (leave it if it's code, a literal, not a clause join, or reads more clearly than a period or a joining word)\",\n 'label-colon':\n 'may use a colon after a label (leave it if it introduces a list or code, defines a term, is notation like a time or `key: value`, or is a `Note:` or `Optional:` that starts the comment)',\n emphasis:\n 'may use asterisk emphasis (leave the asterisks of a name or pattern like `*test*`, which belongs in a code span with its asterisks)',\n dash: \"may use a spaced hyphen, en-dash, or double hyphen as an em-dash (leave it if it's a range, a CLI argument, arithmetic, code, a term's definition, or reads more clearly than the alternatives)\",\n};\n\nconst HINT_MARKER = 'possible:';\n\nfunction describeIssue(issue: IssueReport, width: number, maxWidth?: number): string {\n if (issue.type === 'overflow') {\n const limit = maxWidth ?? width;\n const name = maxWidth === undefined ? 'width' : 'maximum';\n\n return `line ${issue.line} is ${issue.length} columns, over the ${name} of ${limit}`;\n }\n\n const description = `line ${issue.line} ${ISSUE_DESCRIPTIONS[issue.type]}`;\n\n return issue.tier === 'hint' ? `${HINT_MARKER} ${description}` : description;\n}\n\nfunction describeIssues(paragraph: ParagraphReport, width: number, maxWidth?: number): string[] {\n return paragraph.issues.map((issue) => describeIssue(issue, width, maxWidth));\n}\n\nfunction describeComment(comment: CommentReport): string {\n const notes: string[] = [];\n\n for (const paragraph of comment.paragraphs) {\n const issues = describeIssues(paragraph, comment.width, comment.maxWidth);\n\n if (issues.length > 0 && !paragraph.reflowable) {\n const range = `lines ${paragraph.startLine}-${paragraph.endLine}`;\n\n issues.push(\n `${range} are kept as written (${paragraph.reason ?? 'layout'}), so the tool never rewraps them and any fix to them is yours`,\n );\n }\n\n notes.push(...issues);\n }\n\n const fence = comment.text.includes('```') ? '~~~~' : '```';\n\n return [\n `### ${comment.file}:${comment.startLine}-${comment.endLine} (width ${comment.width}${comment.maxWidth === undefined ? '' : `, maximum ${comment.maxWidth}`})`,\n '',\n ...(notes.length > 0 ? notes.map((note) => `- ${note}`) : ['- no measured issues; review the wording and flow']),\n '',\n `${fence}ts`,\n comment.text,\n fence,\n ].join('\\n');\n}\n\nexport function loadRules(agent: AgentConfig, configPath: string | undefined): string {\n let rules = DEFAULT_RULES;\n\n if (agent.rules) {\n const base = configPath ? path.dirname(configPath) : process.cwd();\n\n rules = readFileSync(path.resolve(base, agent.rules), 'utf8');\n }\n\n return agent.instructions ? `${rules.trimEnd()}\\n\\n## Instructions\\n\\n${agent.instructions.trim()}\\n` : rules;\n}\n\nexport function buildPrompt(options: {\n readonly rules: string;\n readonly root: string;\n readonly comments: readonly CommentReport[];\n}): string {\n return [\n options.rules.trimEnd(),\n '',\n '## Task',\n '',\n `Fix the formatting of the comments below, following the rules above. Paths are relative to ${options.root}. Widths include indentation and the comment prefix. The notes under each comment come from the tool's measurements, so never count columns yourself.`,\n '',\n '- After you finish, the tool rewraps every prose paragraph as described under Layout, so a layout note on a prose paragraph needs no change from you. Change only the wording there, and leave its line breaks to the tool.',\n '- A note that a line may end with the start of a sentence, clause, or parenthetical is the exception. Only you can tell whether it does, so if it does, move those words to the start of the next line.',\n '- Parts kept as written are never rewrapped, and their notes say so. Fix those notes by hand, for example by breaking an overflowing prose line or by turning a long single-line comment into a multi-line one. Keep code, code spans, tables, box drawings, hard line breaks, and other intentional layout in them exactly as they are. If a note can only be fixed by changing those, leave it and say why in your reply.',\n `- Notes marked \\`${HINT_MARKER}\\` come from heuristics. If the rule doesn't actually apply, don't change anything for that note.`,\n '- Notes only point at places worth a look. Review each comment against the rules, and leave it as it is when it already follows them.',\n \"- Edit only these comments. Don't change code, other comments, or other files.\",\n '- When done, reply with one line per comment saying what you changed or why you left it.',\n '',\n ...options.comments.map((comment) => describeComment(comment)).flatMap((section) => [section, '']),\n ].join('\\n');\n}\n\nexport function shellQuote(value: string): string {\n return /^[\\w./:@=-]+$/.test(value) ? value : `'${value.replaceAll(\"'\", String.raw`'\\''`)}'`;\n}\n\nexport function buildInvocation(options: {\n readonly agent: AgentConfig;\n readonly runner: AgentRunner;\n readonly prompt: string;\n readonly root: string;\n}): AgentInvocation {\n const { agent, prompt, root } = options;\n const extra = agent.args ?? [];\n\n if (agent.command && agent.command.length > 0) {\n const [command, ...args] = agent.command;\n const hasPlaceholder = args.some((arg) => arg.includes('{prompt}'));\n\n return {\n command,\n // a function keeps the prompt as it is, while a string would expand `$&` or `$'` in a comment's text\n args: args.map((arg) => arg.replaceAll('{prompt}', () => prompt)),\n stdin: hasPlaceholder ? undefined : prompt,\n };\n }\n\n const model = agent.model ? ['--model', agent.model] : [];\n\n // the agent only reads and edits files, so it gets no shell\n if (options.runner === 'claude') {\n return {\n command: 'claude',\n args: [\n '-p',\n '--no-session-persistence',\n '--model',\n agent.model ?? DEFAULT_CLAUDE_MODEL,\n '--permission-mode',\n 'acceptEdits',\n '--allowedTools',\n 'Read',\n 'Edit',\n 'Grep',\n 'Glob',\n ...extra,\n ],\n stdin: prompt,\n };\n }\n\n if (options.runner === 'codex') {\n return {\n command: 'codex',\n args: [\n 'exec',\n '--ephemeral',\n '--sandbox',\n 'workspace-write',\n '--cd',\n root,\n '-c',\n 'features.hooks=false',\n ...model,\n ...extra,\n '-',\n ],\n stdin: prompt,\n };\n }\n\n // cursor-agent has no flag to skip hooks, so the child relies on the `COMMENTS_HOOK_CHILD` guard\n return {\n command: 'cursor-agent',\n args: ['-p', '--trust', '--force', '--workspace', root, ...model, ...extra, prompt],\n };\n}\n\nexport function describeAgent(agent: AgentConfig, runner: AgentRunner): string {\n if (agent.command && agent.command.length > 0) {\n return path.basename(agent.command[0]);\n }\n\n const model = runner === 'claude' ? (agent.model ?? DEFAULT_CLAUDE_MODEL) : agent.model;\n\n return model ? `${RUNNER_COMMANDS[runner]}, model ${model}` : RUNNER_COMMANDS[runner];\n}\n\nexport function formatInvocation(invocation: AgentInvocation): string {\n const args = invocation.args.map((arg) => (arg.length > 80 ? '<prompt>' : shellQuote(arg)));\n\n return [invocation.command, ...args].join(' ');\n}\n\nexport function runAgent(\n invocation: AgentInvocation,\n options: { root: string; timeout?: number },\n): Promise<AgentResult> {\n const timeout = options.timeout ?? DEFAULT_TIMEOUT_SECONDS;\n\n return new Promise((resolve) => {\n const child = spawn(invocation.command, invocation.args, {\n cwd: options.root,\n env: { ...process.env, [CHILD_ENV]: '1' },\n });\n\n const timers: NodeJS.Timeout[] = [];\n\n let stdout = '';\n let stderr = '';\n let timedOut = false;\n let settled = false;\n\n const settle = (result: Omit<AgentResult, 'output'>): void => {\n if (settled) {\n return;\n }\n\n settled = true;\n\n for (const timer of timers) {\n clearTimeout(timer);\n }\n\n // pipes that a process left behind by the agent still holds would keep this process from exiting\n child.stdout.destroy();\n child.stderr.destroy();\n child.unref();\n\n resolve({ ...result, output: `${stdout}${stderr}`.trim() });\n };\n\n const finish = (code: number | null, signal: NodeJS.Signals | null): void => {\n if (timedOut) {\n settle({ ok: false, error: `the agent timed out after ${timeout}s` });\n } else if (code === 0) {\n settle({ ok: true });\n } else {\n settle({ ok: false, error: `the agent exited with code ${code ?? signal}` });\n }\n };\n\n timers.push(\n setTimeout(() => {\n timedOut = true;\n\n child.kill();\n\n // an agent that ignores the signal is killed, and it counts as stopped even if that fails too\n timers.push(\n setTimeout(\n () => {\n child.kill('SIGKILL');\n finish(null, 'SIGKILL');\n },\n Math.min(KILL_GRACE_MS, timeout * 1000),\n ),\n );\n }, timeout * 1000),\n );\n\n child.stdout.setEncoding('utf8').on('data', (chunk: string) => {\n stdout += chunk;\n });\n\n child.stderr.setEncoding('utf8').on('data', (chunk: string) => {\n stderr += chunk;\n });\n\n child.on('error', (error: NodeJS.ErrnoException) => {\n settle({\n ok: false,\n error: error.code === 'ENOENT' ? `\\`${invocation.command}\\` is not installed` : error.message,\n });\n });\n\n child.on('close', finish);\n\n // `close` waits for the pipes, so it may come long after the agent exited, or never\n child.on('exit', (code, signal) => {\n timers.push(\n setTimeout(() => {\n finish(code, signal);\n }, DRAIN_GRACE_MS),\n );\n });\n\n // an agent that exits without reading its stdin would otherwise crash the process with EPIPE\n child.stdin.on('error', () => {});\n child.stdin.end(invocation.stdin ?? '');\n });\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nexport type AgentRunner = 'claude' | 'codex' | 'cursor';\nexport type HookMode = 'agent' | 'report' | 'off';\nexport type AgentScope = 'suspicious' | 'all';\n\nexport interface AgentConfig {\n /**\n * Names the agent CLI to spawn. Defaults to the host that triggered the hook, or `claude`\n * when run standalone.\n */\n readonly runner?: AgentRunner;\n\n /**\n * Names the model passed to the runner. Defaults to `haiku` for Claude Code and to the\n * CLI's default otherwise.\n */\n readonly model?: string;\n\n /**\n * Lists extra arguments appended to the runner's command line.\n */\n readonly args?: readonly string[];\n\n /**\n * Sets a fully custom command, e.g. `['my-agent', '--prompt', '{prompt}']`. Without a `{prompt}`\n * placeholder, the prompt is written to stdin. Overrides `runner`, `model`, and `args`.\n */\n readonly command?: readonly string[];\n\n /**\n * Sets how many seconds to wait for each agent before giving up. Defaults to 300.\n */\n readonly timeout?: number;\n\n /**\n * Sets the most comments to hand to one agent. More comments are split across several agents.\n * A file's comments always go to the same agent, since parallel agents editing one file would\n * overwrite each other's edits, so a file with more comments than this gets an agent of its\n * own. Defaults to 20.\n */\n readonly batchSize?: number;\n\n /**\n * Sets the most files to hand to one agent, since an agent editing many files is slower and less\n * careful. More files are split across several agents. Defaults to 10.\n */\n readonly maxFiles?: number;\n\n /**\n * Caps how many agents run at once. The other agents wait, and each one starts when a running\n * agent finishes. Defaults to 6.\n */\n readonly maxAgents?: number;\n\n /**\n * Points to a Markdown file with formatting rules, relative to the config file.\n * Replaces the built-in rules.\n */\n readonly rules?: string;\n\n /**\n * Holds extra instructions appended to the rules.\n */\n readonly instructions?: string;\n\n /**\n * Selects which comments the agent sees. `suspicious` (default) is a triage, not a full review.\n * It only sends comments with hints, like a possible semicolon or a line that may end with the\n * first word of a sentence, and comments with issues in parts the tool never rewraps, so most\n * runs never call the agent. The tool fixes the layout of prose itself. `all` sends every comment\n * in changed lines, so that the agent reviews the wording of each against its rules.\n */\n readonly scope?: AgentScope;\n}\n\nexport interface HookConfig {\n /**\n * Sets what the stop hook does with comments in changed lines.\n *\n * - `agent` (default): hand them to an agent and report any that still don't fit afterwards.\n * - `report`: only report suspicious comments.\n * - `off`: do nothing.\n */\n readonly mode?: HookMode;\n}\n\nexport interface CommentsConfig {\n /**\n * Sets the preferred line length for comments, including indentation and the comment prefix.\n * Defaults to prettier's `printWidth`, or 80.\n */\n readonly width?: number;\n\n /**\n * Sets the hard limit for measured prose. Defaults to 120, or `width` when it is larger.\n * Parentheticals may use the space between `width` and this limit to stay together.\n */\n readonly maxWidth?: number;\n\n /**\n * Sets the maximum length of the line that holds a comment whose whole text is one prose\n * paragraph. Such a comment stays on one line up to this length, even past `width`, and is\n * wrapped to `width` beyond it. Defaults to 120 and never exceeds `maxWidth`.\n */\n readonly singleLineWidth?: number;\n\n /**\n * Sets a legacy hard limit of `width + tolerance` when `maxWidth` is omitted.\n * An explicit `maxWidth` takes precedence.\n */\n readonly tolerance?: number;\n\n /**\n * Keep the first word of a new sentence or parenthetical off the end of a line, e.g.\n * `...the module. This` or `...reflows (the` followed by the rest on the next line.\n * A paragraph of two lines also ends its first line with a sentence when the rest fits\n * on the second line and is no longer than the first, instead of with the start of the\n * next sentence, like `...the module. This one`. Defaults to `true`.\n */\n readonly avoidSentenceOrphans?: boolean;\n\n /**\n * Looks for possible style issues (semicolons, em-dashes and dashes, `label: explanation` colons,\n * asterisk emphasis, and section banners) and reports them as hints, so that the `suspicious` scope\n * hands those comments to the agent too. It only controls this search, and the agent's rules apply\n * to every comment the agent sees either way. Defaults to `true`.\n */\n readonly style?: boolean;\n\n /**\n * Lists the file extensions to process. Defaults to JavaScript and TypeScript extensions.\n */\n readonly extensions?: readonly string[];\n\n /**\n * Lists glob patterns, relative to the repository root, of files to skip.\n */\n readonly ignore?: readonly string[];\n readonly agent?: AgentConfig;\n readonly hook?: HookConfig;\n}\n\nexport interface LoadedConfig {\n readonly config: CommentsConfig;\n readonly filepath?: string;\n}\n\nexport const DEFAULT_EXTENSIONS = Object.freeze(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts']);\n\nconst CONFIG_FILES = [\n 'comments.config.ts',\n 'comments.config.mts',\n 'comments.config.js',\n 'comments.config.mjs',\n 'comments.config.cjs',\n '.commentsrc',\n '.commentsrc.json',\n];\n\nconst PACKAGE_JSON_KEY = 'comments';\n\nexport function defineConfig(config: CommentsConfig): CommentsConfig {\n return config;\n}\n\nfunction readJson(filepath: string): unknown {\n try {\n return JSON.parse(readFileSync(filepath, 'utf8'));\n } catch (error) {\n throw new Error(`comments: failed to parse ${filepath}: ${(error as Error).message}`, { cause: error });\n }\n}\n\nasync function readConfigFile(filepath: string): Promise<unknown> {\n if (filepath.endsWith('rc') || filepath.endsWith('.json')) {\n return readJson(filepath);\n }\n\n const module = (await import(pathToFileURL(filepath).href)) as { default?: unknown };\n\n return module.default ?? module;\n}\n\nfunction assertType(value: unknown, type: string, key: string, filepath: string): void {\n const actual = Array.isArray(value) ? 'array' : typeof value;\n\n if (value !== undefined && actual !== type) {\n throw new Error(`comments: \\`${key}\\` in ${filepath} must be a ${type}, got ${actual}`);\n }\n}\n\nfunction assertStrings(value: unknown, key: string, filepath: string): void {\n if (Array.isArray(value) && !value.every((item) => typeof item === 'string')) {\n throw new Error(`comments: \\`${key}\\` in ${filepath} must only hold strings`);\n }\n}\n\nexport function validateConfig(raw: unknown, filepath: string): CommentsConfig {\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {\n throw new Error(`comments: ${filepath} must export an object`);\n }\n\n const config = raw as Record<string, unknown>;\n\n assertType(config.width, 'number', 'width', filepath);\n assertType(config.maxWidth, 'number', 'maxWidth', filepath);\n assertType(config.singleLineWidth, 'number', 'singleLineWidth', filepath);\n assertType(config.tolerance, 'number', 'tolerance', filepath);\n assertType(config.avoidSentenceOrphans, 'boolean', 'avoidSentenceOrphans', filepath);\n assertType(config.style, 'boolean', 'style', filepath);\n assertType(config.extensions, 'array', 'extensions', filepath);\n assertType(config.ignore, 'array', 'ignore', filepath);\n assertType(config.agent, 'object', 'agent', filepath);\n assertType(config.hook, 'object', 'hook', filepath);\n assertStrings(config.extensions, 'extensions', filepath);\n assertStrings(config.ignore, 'ignore', filepath);\n\n for (const key of ['width', 'maxWidth', 'singleLineWidth', 'tolerance'] as const) {\n const value = config[key];\n const minimum = key === 'tolerance' ? 0 : 1;\n\n if (value !== undefined && (!Number.isInteger(value) || (value as number) < minimum)) {\n throw new Error(`comments: \\`${key}\\` in ${filepath} must be an integer of at least ${minimum}`);\n }\n }\n\n if (typeof config.width === 'number' && typeof config.maxWidth === 'number' && config.maxWidth < config.width) {\n throw new Error(`comments: \\`maxWidth\\` in ${filepath} must be at least \\`width\\``);\n }\n\n const agent = (config.agent ?? {}) as Record<string, unknown>;\n\n if (agent.runner !== undefined && !['claude', 'codex', 'cursor'].includes(agent.runner as string)) {\n throw new Error(`comments: \\`agent.runner\\` in ${filepath} must be one of claude, codex, cursor`);\n }\n\n assertType(agent.model, 'string', 'agent.model', filepath);\n assertType(agent.args, 'array', 'agent.args', filepath);\n assertType(agent.command, 'array', 'agent.command', filepath);\n assertType(agent.timeout, 'number', 'agent.timeout', filepath);\n assertStrings(agent.args, 'agent.args', filepath);\n assertStrings(agent.command, 'agent.command', filepath);\n\n // a timeout of zero would stop every agent right away, and `NaN` is a number too\n if (agent.timeout !== undefined && !((agent.timeout as number) > 0)) {\n throw new Error(`comments: \\`agent.timeout\\` in ${filepath} must be a positive number`);\n }\n\n for (const key of ['batchSize', 'maxFiles', 'maxAgents'] as const) {\n const value = agent[key];\n\n if (value !== undefined && (!Number.isInteger(value) || (value as number) < 1)) {\n throw new Error(`comments: \\`agent.${key}\\` in ${filepath} must be a positive integer`);\n }\n }\n\n assertType(agent.rules, 'string', 'agent.rules', filepath);\n assertType(agent.instructions, 'string', 'agent.instructions', filepath);\n\n if (agent.scope !== undefined && agent.scope !== 'suspicious' && agent.scope !== 'all') {\n throw new Error(`comments: \\`agent.scope\\` in ${filepath} must be \"suspicious\" or \"all\"`);\n }\n\n const hook = (config.hook ?? {}) as Record<string, unknown>;\n\n if (hook.mode !== undefined && !['agent', 'report', 'off'].includes(hook.mode as string)) {\n throw new Error(`comments: \\`hook.mode\\` in ${filepath} must be one of agent, report, off`);\n }\n\n return config as CommentsConfig;\n}\n\nexport async function loadConfig(cwd: string, options: { explicitPath?: string; stopDir?: string } = {}) {\n if (options.explicitPath) {\n const filepath = path.resolve(cwd, options.explicitPath);\n\n return { config: validateConfig(await readConfigFile(filepath), filepath), filepath } satisfies LoadedConfig;\n }\n\n let directory = path.resolve(cwd);\n\n for (;;) {\n for (const name of CONFIG_FILES) {\n const filepath = path.join(directory, name);\n\n if (existsSync(filepath)) {\n return { config: validateConfig(await readConfigFile(filepath), filepath), filepath } satisfies LoadedConfig;\n }\n }\n\n const packageJsonPath = path.join(directory, 'package.json');\n\n if (existsSync(packageJsonPath)) {\n const value = (readJson(packageJsonPath) as Record<string, unknown>)[PACKAGE_JSON_KEY];\n\n if (value !== undefined) {\n return { config: validateConfig(value, packageJsonPath), filepath: packageJsonPath } satisfies LoadedConfig;\n }\n }\n\n const parent = path.dirname(directory);\n\n if (directory === options.stopDir || parent === directory) {\n return { config: {} } satisfies LoadedConfig;\n }\n\n directory = parent;\n }\n}\n\ninterface PrettierOptions {\n readonly printWidth?: number;\n readonly tabWidth?: number;\n}\n\ninterface PrettierModule {\n resolveConfig: (file: string) => Promise<PrettierOptions | null>;\n}\n\nconst PRETTIER_FILES = ['.prettierrc', '.prettierrc.json', '.prettierrc.json5'];\n\nfunction readPrettierFile(directory: string, stopDir: string): PrettierOptions | undefined {\n let current = directory;\n\n for (;;) {\n for (const name of PRETTIER_FILES) {\n const filepath = path.join(current, name);\n\n if (!existsSync(filepath)) {\n continue;\n }\n\n const text = readFileSync(filepath, 'utf8');\n\n // `.prettierrc` may be YAML, so fall back to plain key lookups\n const printWidth = /[\"']?printWidth[\"']?\\s*:\\s*(\\d+)/.exec(text)?.[1];\n const tabWidth = /[\"']?tabWidth[\"']?\\s*:\\s*(\\d+)/.exec(text)?.[1];\n\n return {\n printWidth: printWidth === undefined ? undefined : Number(printWidth),\n tabWidth: tabWidth === undefined ? undefined : Number(tabWidth),\n };\n }\n\n const packageJsonPath = path.join(current, 'package.json');\n\n if (existsSync(packageJsonPath)) {\n const prettier = (readJson(packageJsonPath) as { prettier?: unknown }).prettier;\n\n if (prettier && typeof prettier === 'object') {\n return prettier as PrettierOptions;\n }\n }\n\n const parent = path.dirname(current);\n\n if (current === stopDir || parent === current) {\n return undefined;\n }\n\n current = parent;\n }\n}\n\nexport class PrettierOptionsResolver {\n private _prettier: PrettierModule | null | undefined;\n private readonly _cache = new Map<string, PrettierOptions>();\n\n constructor(private readonly _root: string) {}\n\n private async _loadPrettier(): Promise<PrettierModule | null> {\n if (this._prettier !== undefined) {\n return this._prettier;\n }\n\n try {\n const require = createRequire(path.join(this._root, 'package.json'));\n const resolved = require.resolve('prettier');\n\n this._prettier = (await import(pathToFileURL(resolved).href)) as PrettierModule;\n } catch {\n this._prettier = null;\n }\n\n return this._prettier;\n }\n\n async resolve(file: string): Promise<PrettierOptions> {\n const prettier = await this._loadPrettier();\n\n /**\n * Prettier resolves per file, since `overrides` can give files in the same\n * directory different widths.\n */\n if (prettier) {\n try {\n const options = await prettier.resolveConfig(file);\n\n if (options) {\n return options;\n }\n } catch {\n // fall back to reading the config files directly\n }\n }\n\n const directory = path.dirname(file);\n const cached = this._cache.get(directory);\n\n if (cached) {\n return cached;\n }\n\n const options = readPrettierFile(directory, this._root) ?? {};\n\n this._cache.set(directory, options);\n\n return options;\n }\n}\n"]}
@@ -0,0 +1,257 @@
1
+ type StyleIssueType = 'semicolon' | 'em-dash' | 'dash' | 'label-colon' | 'emphasis' | 'banner';
2
+
3
+ interface WrapOptions {
4
+ /**
5
+ * Sets whether a line may not end with the first word of a sentence, clause, or parenthetical,
6
+ * like `This`, `the` after a comma, or `(the`. Such a word moves to the next line instead.
7
+ * It also sets whether the first of two lines ends with a sentence instead of the start of the
8
+ * next one, as long as the second line is then no longer than the first.
9
+ */
10
+ readonly avoidSentenceOrphans: boolean;
11
+ /**
12
+ * Sets how many columns a line may exceed its limit before it counts as overflowing.
13
+ */
14
+ readonly tolerance: number;
15
+ /**
16
+ * Sets how many extra columns a complete parenthetical may use beyond the preferred line limit.
17
+ */
18
+ readonly parentheticalOverflow?: number;
19
+ }
20
+ type LineIssueType = 'overflow' | 'underfill' | 'orphan' | 'widow' | 'unbalanced';
21
+ /**
22
+ * JSDoc writes types and `{@link ...}` tags as `{...}` groups, so they stay intact like code spans.
23
+ */
24
+ declare function tokenize(text: string): string[];
25
+ /**
26
+ * The first line can have a different limit than the rest, e.g. when a list marker takes up space
27
+ * only on the first line.
28
+ */
29
+ declare function wrapTokens(tokens: readonly string[], firstLimit: number, restLimit: number, options: WrapOptions): string[][];
30
+
31
+ type CommentKind = 'jsdoc' | 'block';
32
+ type ParagraphKind = 'prose' | 'list' | 'tag' | 'code' | 'other';
33
+ interface AnalyzeOptions extends WrapOptions {
34
+ /**
35
+ * Sets the preferred line length, including indentation and the comment prefix.
36
+ */
37
+ readonly width: number;
38
+ /**
39
+ * Sets the hard limit for measured prose. Parentheticals may exceed `width` up to this limit.
40
+ * When omitted, the existing width and tolerance behavior is retained.
41
+ */
42
+ readonly maxWidth?: number;
43
+ /**
44
+ * Sets the maximum length of the line that holds a comment whose whole text is one
45
+ * prose paragraph. Such a comment stays on one line up to this length, even past
46
+ * `width`. It defaults to `width`.
47
+ */
48
+ readonly singleLineWidth?: number;
49
+ readonly tabWidth: number;
50
+ /**
51
+ * Whether to report prose style issues like semicolons, em-dashes, and section banners.
52
+ */
53
+ readonly style: boolean;
54
+ }
55
+ type IssueType = LineIssueType | StyleIssueType;
56
+ type IssueTier = 'verdict' | 'hint';
57
+ interface IssueReport {
58
+ readonly type: IssueType;
59
+ readonly tier: IssueTier;
60
+ readonly line: number;
61
+ readonly length: number;
62
+ }
63
+ interface ParagraphReport {
64
+ readonly kind: ParagraphKind;
65
+ readonly startLine: number;
66
+ readonly endLine: number;
67
+ /**
68
+ * Whether the paragraph is prose that can be rewrapped. When `false`, `reason` says why.
69
+ */
70
+ readonly reflowable: boolean;
71
+ readonly reason?: string;
72
+ readonly issues: readonly IssueReport[];
73
+ /**
74
+ * Holds the exact source lines for `startLine` through `endLine` when the paragraph is
75
+ * greedily wrapped to the width, or `null` when the paragraph isn't prose, e.g. code,
76
+ * a table, a diagram, or `@example`.
77
+ */
78
+ readonly suggestion: readonly string[] | null;
79
+ }
80
+ interface CommentReport {
81
+ readonly file: string;
82
+ readonly kind: CommentKind;
83
+ readonly startLine: number;
84
+ readonly endLine: number;
85
+ readonly indent: string;
86
+ readonly width: number;
87
+ readonly maxWidth?: number;
88
+ /**
89
+ * Holds the length of each source line from `startLine` through `endLine`, with tabs expanded.
90
+ */
91
+ readonly lengths: readonly number[];
92
+ readonly text: string;
93
+ readonly paragraphs: readonly ParagraphReport[];
94
+ }
95
+ /**
96
+ * Finds all own-line block and JSDoc comments in a JavaScript or TypeScript source file and reports
97
+ * how each paragraph deviates from a greedy wrap at `options.width`. Line comments (`//`) are never
98
+ * analyzed. A source with a syntax error that stops the parser has no reports.
99
+ */
100
+ declare function analyzeSource(source: string, filename: string, options: AnalyzeOptions): CommentReport[];
101
+ /**
102
+ * Whether the comment has any issue, including hints, which makes it worth handing to the agent.
103
+ */
104
+ declare function isSuspicious(report: CommentReport): boolean;
105
+ /**
106
+ * Whether the comment has an issue that isn't a hint, so it breaks the rules no matter what
107
+ * the agent decided.
108
+ */
109
+ declare function hasVerdictIssues(report: CommentReport): boolean;
110
+ /**
111
+ * Returns the comment's source text with every prose paragraph replaced by its suggestion.
112
+ * Code, tables, diagrams, and similar paragraphs are kept as they are.
113
+ */
114
+ declare function rewrapComment(report: CommentReport): string;
115
+ /**
116
+ * Returns the syntax tree of the source as JSON, without positions or comments, used to verify
117
+ * that only comments changed. Moving or rewording a comment keeps it the same, while any change
118
+ * to a string, template, regex, JSX text, or where a statement ends changes it. A source that
119
+ * doesn't parse has no tree to compare, so all of its text counts.
120
+ */
121
+ declare function codeFingerprint(source: string, filename: string): string;
122
+
123
+ type AgentRunner = 'claude' | 'codex' | 'cursor';
124
+ type HookMode = 'agent' | 'report' | 'off';
125
+ type AgentScope = 'suspicious' | 'all';
126
+ interface AgentConfig {
127
+ /**
128
+ * Names the agent CLI to spawn. Defaults to the host that triggered the hook, or `claude`
129
+ * when run standalone.
130
+ */
131
+ readonly runner?: AgentRunner;
132
+ /**
133
+ * Names the model passed to the runner. Defaults to `haiku` for Claude Code and to the
134
+ * CLI's default otherwise.
135
+ */
136
+ readonly model?: string;
137
+ /**
138
+ * Lists extra arguments appended to the runner's command line.
139
+ */
140
+ readonly args?: readonly string[];
141
+ /**
142
+ * Sets a fully custom command, e.g. `['my-agent', '--prompt', '{prompt}']`. Without a `{prompt}`
143
+ * placeholder, the prompt is written to stdin. Overrides `runner`, `model`, and `args`.
144
+ */
145
+ readonly command?: readonly string[];
146
+ /**
147
+ * Sets how many seconds to wait for each agent before giving up. Defaults to 300.
148
+ */
149
+ readonly timeout?: number;
150
+ /**
151
+ * Sets the most comments to hand to one agent. More comments are split across several agents.
152
+ * A file's comments always go to the same agent, since parallel agents editing one file would
153
+ * overwrite each other's edits, so a file with more comments than this gets an agent of its
154
+ * own. Defaults to 20.
155
+ */
156
+ readonly batchSize?: number;
157
+ /**
158
+ * Sets the most files to hand to one agent, since an agent editing many files is slower and less
159
+ * careful. More files are split across several agents. Defaults to 10.
160
+ */
161
+ readonly maxFiles?: number;
162
+ /**
163
+ * Caps how many agents run at once. The other agents wait, and each one starts when a running
164
+ * agent finishes. Defaults to 6.
165
+ */
166
+ readonly maxAgents?: number;
167
+ /**
168
+ * Points to a Markdown file with formatting rules, relative to the config file.
169
+ * Replaces the built-in rules.
170
+ */
171
+ readonly rules?: string;
172
+ /**
173
+ * Holds extra instructions appended to the rules.
174
+ */
175
+ readonly instructions?: string;
176
+ /**
177
+ * Selects which comments the agent sees. `suspicious` (default) is a triage, not a full review.
178
+ * It only sends comments with hints, like a possible semicolon or a line that may end with the
179
+ * first word of a sentence, and comments with issues in parts the tool never rewraps, so most
180
+ * runs never call the agent. The tool fixes the layout of prose itself. `all` sends every comment
181
+ * in changed lines, so that the agent reviews the wording of each against its rules.
182
+ */
183
+ readonly scope?: AgentScope;
184
+ }
185
+ interface HookConfig {
186
+ /**
187
+ * Sets what the stop hook does with comments in changed lines.
188
+ *
189
+ * - `agent` (default): hand them to an agent and report any that still don't fit afterwards.
190
+ * - `report`: only report suspicious comments.
191
+ * - `off`: do nothing.
192
+ */
193
+ readonly mode?: HookMode;
194
+ }
195
+ interface CommentsConfig {
196
+ /**
197
+ * Sets the preferred line length for comments, including indentation and the comment prefix.
198
+ * Defaults to prettier's `printWidth`, or 80.
199
+ */
200
+ readonly width?: number;
201
+ /**
202
+ * Sets the hard limit for measured prose. Defaults to 120, or `width` when it is larger.
203
+ * Parentheticals may use the space between `width` and this limit to stay together.
204
+ */
205
+ readonly maxWidth?: number;
206
+ /**
207
+ * Sets the maximum length of the line that holds a comment whose whole text is one prose
208
+ * paragraph. Such a comment stays on one line up to this length, even past `width`, and is
209
+ * wrapped to `width` beyond it. Defaults to 120 and never exceeds `maxWidth`.
210
+ */
211
+ readonly singleLineWidth?: number;
212
+ /**
213
+ * Sets a legacy hard limit of `width + tolerance` when `maxWidth` is omitted.
214
+ * An explicit `maxWidth` takes precedence.
215
+ */
216
+ readonly tolerance?: number;
217
+ /**
218
+ * Keep the first word of a new sentence or parenthetical off the end of a line, e.g.
219
+ * `...the module. This` or `...reflows (the` followed by the rest on the next line.
220
+ * A paragraph of two lines also ends its first line with a sentence when the rest fits
221
+ * on the second line and is no longer than the first, instead of with the start of the
222
+ * next sentence, like `...the module. This one`. Defaults to `true`.
223
+ */
224
+ readonly avoidSentenceOrphans?: boolean;
225
+ /**
226
+ * Looks for possible style issues (semicolons, em-dashes and dashes, `label: explanation` colons,
227
+ * asterisk emphasis, and section banners) and reports them as hints, so that the `suspicious` scope
228
+ * hands those comments to the agent too. It only controls this search, and the agent's rules apply
229
+ * to every comment the agent sees either way. Defaults to `true`.
230
+ */
231
+ readonly style?: boolean;
232
+ /**
233
+ * Lists the file extensions to process. Defaults to JavaScript and TypeScript extensions.
234
+ */
235
+ readonly extensions?: readonly string[];
236
+ /**
237
+ * Lists glob patterns, relative to the repository root, of files to skip.
238
+ */
239
+ readonly ignore?: readonly string[];
240
+ readonly agent?: AgentConfig;
241
+ readonly hook?: HookConfig;
242
+ }
243
+ declare function defineConfig(config: CommentsConfig): CommentsConfig;
244
+ declare function loadConfig(cwd: string, options?: {
245
+ explicitPath?: string;
246
+ stopDir?: string;
247
+ }): Promise<{
248
+ config: CommentsConfig;
249
+ filepath: string;
250
+ } | {
251
+ config: {};
252
+ filepath?: undefined;
253
+ }>;
254
+
255
+ declare const DEFAULT_RULES: string;
256
+
257
+ export { type AgentConfig, type AgentRunner, type AgentScope, type AnalyzeOptions, type CommentKind, type CommentReport, type CommentsConfig, DEFAULT_RULES, type HookConfig, type HookMode, type IssueReport, type IssueTier, type ParagraphKind, type ParagraphReport, analyzeSource, codeFingerprint, defineConfig, hasVerdictIssues, isSuspicious, loadConfig, rewrapComment, tokenize, wrapTokens };