@descent-vtt/spec-graph 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +146 -0
- package/README.md +162 -15
- package/dist/baseline.js +6 -0
- package/dist/baseline.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +50 -17
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +3 -3
- package/dist/config.js +16 -8
- package/dist/config.js.map +1 -1
- package/dist/diff.js +14 -0
- package/dist/diff.js.map +1 -1
- package/dist/directives.js +9 -6
- package/dist/directives.js.map +1 -1
- package/dist/extract.js +120 -57
- package/dist/extract.js.map +1 -1
- package/dist/glob.js +28 -16
- package/dist/glob.js.map +1 -1
- package/dist/graph.d.ts +2 -1
- package/dist/graph.js +10 -1
- package/dist/graph.js.map +1 -1
- package/dist/identity.d.ts +6 -0
- package/dist/identity.js +68 -68
- package/dist/identity.js.map +1 -1
- package/dist/lifecycle.js +17 -11
- package/dist/lifecycle.js.map +1 -1
- package/dist/report.js +34 -5
- package/dist/report.js.map +1 -1
- package/dist/resolve.js +40 -14
- package/dist/resolve.js.map +1 -1
- package/dist/rules.d.ts +8 -0
- package/dist/rules.js +83 -16
- package/dist/rules.js.map +1 -1
- package/dist/runner.js +22 -4
- package/dist/runner.js.map +1 -1
- package/dist/sections.d.ts +9 -0
- package/dist/sections.js +60 -9
- package/dist/sections.js.map +1 -1
- package/dist/select.d.ts +18 -2
- package/dist/select.js +46 -16
- package/dist/select.js.map +1 -1
- package/dist/source.js +2 -0
- package/dist/source.js.map +1 -1
- package/dist/state.js +46 -12
- package/dist/state.js.map +1 -1
- package/dist/yaml.js +11 -3
- package/dist/yaml.js.map +1 -1
- package/package.json +6 -3
- package/src/vendor/spec-core/README.md +0 -8
package/dist/glob.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EACL,WAAW,IAAI,iBAAiB,EAChC,SAAS,EACT,YAAY,EACZ,SAAS,EACT,aAAa,GAKd,MAAM,qCAAqC,CAAC;AAE7C,kEAAkE;AAClE,MAAM,CAAC,MAAM,2BAA2B,GAAsB,MAAM,CAAC,MAAM,CAAC;IAC1E,MAAM;IACN,KAAK;IACL,MAAM;IACN,QAAQ;IACR,OAAO;IACP,OAAO;IACP,QAAQ;IACR,OAAO;IACP,cAAc;IACd,kBAAkB;IAClB,QAAQ;IACR,MAAM;IACN,OAAO;IACP,KAAK;IACL,QAAQ;IACR,UAAU;IACV,aAAa;CACd,CAAC,CAAC;AAEH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAmB7C;;;;;;;GAOG;AACH,MAAM,IAAI,GAAsB,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC;AAEpH,kFAAkF;AAClF,MAAM,UAAU,MAAM,CAAC,OAAe;IACpC,OAAO,YAAY,CAAC,OAAO,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,OAAO,IAAI,MAAM,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AAChD,CAAC;AAOD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,OAAO,GAAgB,EAAE;IACpE,0EAA0E;IAC1E,6EAA6E;IAC7E,gEAAgE;IAChE,MAAM,IAAI,GAAG,iBAAiB,CAAC,OAAO,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC,CAAC;IAClH,OAAO,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;AACjF,CAAC;AAED,8EAA8E;AAC9E,SAAS,UAAU,CAAC,OAAe;IACjC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAW,CAAC;QAEhC,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;YACvC,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;gBAC1C,iEAAiE;gBACjE,MAAM,IAAI,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;gBACzC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1B,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,OAAO,CAAC;gBAClB,CAAC,IAAI,CAAC,CAAC;YACT,CAAC;YACD,SAAS;QACX,CAAC;QAED,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,MAAM,IAAI,MAAM,CAAC;YACjB,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QAED,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1C,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC;gBAChB,CAAC,IAAI,CAAC,CAAC;gBACP,SAAS;YACX,CAAC;YACD,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;YACzC,MAAM,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC;YAC7F,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;YACd,SAAS;QACX,CAAC;QAED,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC;YAChB,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpC,MAAM,CAAC,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,GAAG,CAAC;YACd,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,GAAG,CAAC;YACd,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QAED,MAAM,IAAI,EAAE,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;QAClD,CAAC,IAAI,CAAC,CAAC;IACT,CAAC;IAED,2EAA2E;IAC3E,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,OAAO,iBAAiB,CAAC,CAAC;IAClF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AAC1C,CAAC;AAMD,qFAAqF;AACrF,SAAS,QAAQ,CAAC,QAA2B;IAC3C,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC7C,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IAChE,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAA2B;IAC3D,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAChC,OAAO,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAA2B;IAC/D,0EAA0E;IAC1E,4EAA4E;IAC5E,iEAAiE;IACjE,MAAM,KAAK,GAAW,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7C,MAAM,WAAW,GAAG,OAAO;aACxB,IAAI,EAAE;aACN,KAAK,CAAC,GAAG,CAAC;aACV,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;aACnF,IAAI,CAAC,GAAG,CAAC,CAAC;QACb,uEAAuE;QACvE,qEAAqE;QACrE,0EAA0E;QAC1E,MAAM,MAAM,GAAG,SAAS,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QAClG,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,MAAM,IAAI,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3D,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,MAAc,EAAW,EAAE;QACjC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;QAC5B,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,MAAM,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAA4B,CAAC;IAC3E,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAa,CAAC;IACpC,IAAI,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,uEAAuE;QACvE,yEAAyE;QACzE,OAAO,IAAI,GAAG,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,IAAI,CAAC;YAAE,IAAI,IAAI,CAAC,CAAC;QAC/D,MAAM,GAAG,IAAI,CAAC;IAChB,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;;;GASG;AACH,KAAK,UAAU,eAAe,CAAC,IAAY,EAAE,IAAY;IACvD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,SAAS,GAAG,IAAI,CAAC;IACrB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,IAAI,KAAe,CAAC;QACpB,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,sEAAsE;YACtE,uEAAuE;YACvE,gBAAgB;YAChB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,KAAK,CAAC;QAC3C,SAAS,GAAG,GAAG,SAAS,IAAI,OAAO,EAAE,CAAC;IACxC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAoB;IAClD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACvD,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,OAAO,CAAC,WAAW,IAAI,aAAa,CAAC;IAErD,6EAA6E;IAC7E,yEAAyE;IACzE,sEAAsE;IACtE,8EAA8E;IAC9E,0EAA0E;IAC1E,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;IACrC,6EAA6E;IAC7E,6EAA6E;IAC7E,uEAAuE;IACvE,MAAM,IAAI,GAAG,CAAC,OAAe,EAAW,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC5G,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,2BAA2B,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxF,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEnE,4EAA4E;IAC5E,0EAA0E;IAC1E,2EAA2E;IAC3E,6EAA6E;IAC7E,6EAA6E;IAC7E,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,OAAO;YAAE,SAAS;QAC5B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC;IACD,0EAA0E;IAC1E,qEAAqE;IACrE,uEAAuE;IACvE,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,CAAC,CAAC,CAAC,EAAE,CAAC;QACN,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;IAE7G,MAAM,GAAG,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC1C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAElC,MAAM,IAAI,GAAG,KAAK,EAAE,QAAgB,EAAiB,EAAE;QACrD,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,QAAQ,EAAE,CAAC;QACtE,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO;QAClC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAEtB,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,CAAC;QAAC,MAAM,CAAC;YACP,iEAAiE;YACjE,OAAO;QACT,CAAC;QAED,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;YAE/E,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,kEAAkE;gBAClE,wEAAwE;gBACxE,IAAI,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC;oBAAE,SAAS;gBAC9D,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC;gBAClB,SAAS;YACX,CAAC;YAED,IAAI,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;gBAC3B,IAAI,CAAC,OAAO,CAAC,cAAc;oBAAE,SAAS;gBACtC,IAAI,CAAC;oBACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC;oBAC5C,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;wBACvB,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC;wBAClB,SAAS;oBACX,CAAC;oBACD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,GAAG,OAAO;wBAAE,SAAS;oBAC/E,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,IAAI,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;gBACjF,CAAC;gBAAC,MAAM,CAAC;oBACP,SAAS;gBACX,CAAC;gBACD,SAAS;YACX,CAAC;YAED,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;gBAAE,SAAS;YAC9B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC;gBAAE,SAAS;YAExD,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC;gBAC5C,IAAI,IAAI,CAAC,IAAI,GAAG,OAAO;oBAAE,SAAS;gBAClC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,IAAI,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YACjF,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,IAAI,MAAM,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC;YAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;IAElF,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5F,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,QAAgB;IACtD,IAAI,cAAc,CAAC,QAAQ,CAAC;QAAE,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvD,OAAO,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC;AAC5C,CAAC","sourcesContent":["/**\n * Pattern matching and directory walking.\n *\n * The patterns are the `path` dialect every spec-* tool reads, from spec-core:\n * `**`, `*`, `?`, `{a,b}`, `[abc]` and `[!a-z]`, matched against whole\n * repository-relative POSIX paths by an automaton that cannot backtrack, with\n * case respected on every host (ADR-0022). What stays here is spec-graph's own:\n * how a list with `!` entries in it reads, which directories a walk never\n * enters, and how a reference target - which is not a path - is matched.\n */\n\nimport { readdir, stat } from 'node:fs/promises';\n\nimport { isAbsolutePath, joinPosix, toPosix } from './paths.js';\nimport {\n compileGlob as compileFamilyGlob,\n GlobError,\n isGlobSyntax,\n parseGlob,\n parseGlobList,\n type Glob,\n type GlobList,\n type GlobOptions as FamilyGlobOptions,\n type RegexMatcher,\n} from './vendor/spec-core/pattern/index.js';\n\n/** Directories skipped unless a pattern explicitly names them. */\nexport const DEFAULT_IGNORED_DIRECTORIES: readonly string[] = Object.freeze([\n '.git',\n '.hg',\n '.svn',\n '.cache',\n '.next',\n '.nuxt',\n '.turbo',\n '.venv',\n 'node_modules',\n 'bower_components',\n 'vendor',\n 'dist',\n 'build',\n 'out',\n 'target',\n 'coverage',\n '__pycache__',\n]);\n\n/** Files larger than this are skipped: a specification is not a binary. */\nexport const MAX_FILE_SIZE = 4 * 1024 * 1024;\n\nexport interface WalkedFile {\n /** Repository-relative POSIX path. */\n readonly path: string;\n /** Absolute host path, for reading. */\n readonly absolute: string;\n readonly size: number;\n}\n\nexport interface WalkOptions {\n readonly root: string;\n readonly patterns: readonly string[];\n readonly ignore?: readonly string[] | undefined;\n readonly maxFileSize?: number | undefined;\n /** Follow directory symlinks. Off by default: cycles are real. */\n readonly followSymlinks?: boolean | undefined;\n}\n\n/**\n * How a path pattern is read everywhere in spec-graph.\n *\n * A literal names a file or a directory and everything beneath it, which is\n * what `--ignore docs/drafts` has always meant. A `\\` is a separator, because a\n * pattern typed on a Windows shell arrives with them and spec-graph has always\n * read them that way; it cannot also be an escape.\n */\nconst PATH: FamilyGlobOptions = { dialect: 'path', caseSensitive: true, literal: 'either', backslash: 'separator' };\n\n/** True when the string contains glob syntax rather than being a literal path. */\nexport function isGlob(pattern: string): boolean {\n return isGlobSyntax(pattern);\n}\n\n/**\n * Compiles a glob to an anchored regular expression, in the dialect spec-graph\n * read before it adopted spec-core's.\n *\n * @deprecated Nothing matches with this. It stays for callers who want a\n * `RegExp`, and as the record of the old reading that `tests/glob.test.ts`\n * holds every difference against, each one named in ADR-0022. It no longer\n * folds case on Windows alone: an answer must not depend on the host.\n */\nexport function globToRegExp(pattern: string): RegExp {\n return new RegExp(`^${globSource(pattern)}$`);\n}\n\nexport interface GlobOptions {\n /** Fold case. Off unless asked for, on every host. */\n readonly ignoreCase?: boolean | undefined;\n}\n\n/**\n * Compiles one glob to a matcher over whole repository-relative paths.\n *\n * A pattern with no glob syntax names that path and nothing beneath it; a bare\n * directory meaning its contents is a reading of {@link createGlobMatcher}'s.\n * Throws a `GlobError` that names the glob as it was written.\n *\n * Matching is linear in the path whatever the pattern. It was not always: a\n * glob's stars are not nested, but each is a `[^/]*` to `RegExp`, and a subject\n * that fails is divided between them every way there is - `*-*-*-x` took two\n * minutes over a 10,000-character reference target (ADR-0017).\n */\nexport function compileGlob(pattern: string, options: GlobOptions = {}): RegexMatcher {\n // `literal: 'file'` is the dialect's reading of any value but `directory`\n // and `either`, so a mutant that blanks it reads the same. It is spelled out\n // because what an unknown value means is spec-core's to change.\n const glob = compileFamilyGlob(pattern, { ...PATH, literal: 'file', caseSensitive: options.ignoreCase !== true });\n return { test: (path) => glob.match(path), size: glob.automaton.kinds.length };\n}\n\n/** The expression a glob stood for before 0.9.0, before anchors and flags. */\nfunction globSource(pattern: string): string {\n let source = '';\n let i = 0;\n const braces: number[] = [];\n\n while (i < pattern.length) {\n const ch = pattern[i] as string;\n\n if (ch === '*') {\n const doubled = pattern[i + 1] === '*';\n if (doubled) {\n const slashAfter = pattern[i + 2] === '/';\n // `a/**/b` must also match `a/b`, so the separator is folded in.\n source += slashAfter ? '(?:.*/)?' : '.*';\n i += slashAfter ? 3 : 2;\n } else {\n source += '[^/]*';\n i += 1;\n }\n continue;\n }\n\n if (ch === '?') {\n source += '[^/]';\n i += 1;\n continue;\n }\n\n if (ch === '[') {\n const close = pattern.indexOf(']', i + 1);\n if (close === -1) {\n source += '\\\\[';\n i += 1;\n continue;\n }\n const body = pattern.slice(i + 1, close);\n source += `[${body.startsWith('!') ? `^${escapeClass(body.slice(1))}` : escapeClass(body)}]`;\n i = close + 1;\n continue;\n }\n\n if (ch === '{') {\n braces.push(source.length);\n source += '(?:';\n i += 1;\n continue;\n }\n if (ch === '}' && braces.length > 0) {\n braces.pop();\n source += ')';\n i += 1;\n continue;\n }\n if (ch === ',' && braces.length > 0) {\n source += '|';\n i += 1;\n continue;\n }\n\n source += ch.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&');\n i += 1;\n }\n\n // Otherwise reported as an unmatched parenthesis, in a glob that has none.\n if (braces.length > 0) throw new Error(`invalid glob \"${pattern}\": unclosed \"{\"`);\n return source;\n}\n\nfunction escapeClass(body: string): string {\n return body.replace(/[\\\\^\\]]/g, '\\\\$&');\n}\n\nexport interface GlobMatcher {\n (path: string): boolean;\n}\n\n/** Compiles a list of path patterns, or throws naming the one that is not a glob. */\nfunction pathList(patterns: readonly string[]): GlobList {\n const parsed = parseGlobList(patterns, PATH);\n if (!parsed.ok) throw new Error(`invalid glob ${parsed.error}`);\n return parsed.list;\n}\n\n/**\n * Builds a matcher from include patterns, honouring `!` negations.\n *\n * Later patterns win, so `docs/**\\/*.md` followed by `!docs/drafts/**` reads the\n * way a `.gitignore` does. A pattern with no glob syntax in it names a file, or\n * a directory and everything beneath it; `docs/` names only what is beneath.\n */\nexport function createGlobMatcher(patterns: readonly string[]): GlobMatcher {\n const list = pathList(patterns);\n return (path: string): boolean => list.match(path);\n}\n\n/**\n * Builds a predicate over reference *targets*, not paths.\n *\n * Separate from {@link createGlobMatcher} on purpose. That one is\n * path-oriented: a bare name there is a directory and everything under it. A\n * reference target has no such structure - `trap 55` is a name, not a location\n * - so a bare pattern matches it literally and nothing else.\n *\n * Case is ignored, on every platform, because a citation's spelling is the\n * author's and not the filesystem's. The fold is simple case mapping, one code\n * point at a time and without a locale, so every host gives the same answer:\n * U+00B5 (micro sign) and U+03BC (mu) stay two targets, as they have been since\n * a Windows-only `i` flag made them one there (ADR-0017).\n *\n * A target is text a document holds rather than a path on this host, and two\n * things follow. A `\\` escapes the character after it: there is no Windows\n * separator to allow for. And a `.` or `..` segment is text: in a path the\n * dialect drops the one and refuses the other as climbing out of the root, but\n * `../../notes/gone.md` is a link somebody wrote and may want left alone.\n */\nexport function createReferenceFilter(patterns: readonly string[]): (target: string) => boolean {\n // A blank pattern reaches the dialect, which refuses it, as every pattern\n // spec-graph reads is refused when it names nothing. It used to be dropped,\n // so an unset variable in `--ignore-ref \"$TAGS\"` went unnoticed.\n const globs: Glob[] = patterns.map((pattern) => {\n const literalDots = pattern\n .trim()\n .split('/')\n .map((segment) => (segment === '.' ? '\\\\.' : segment === '..' ? '\\\\.\\\\.' : segment))\n .join('/');\n // Every option spelled out, though the dialect reads `{}` the same way\n // today: its defaults are spec-core's to change, and this reading is\n // spec-graph's to keep. A mutant that drops one is equivalent until then.\n const parsed = parseGlob(literalDots, { dialect: 'path', caseSensitive: false, literal: 'file' });\n if (!parsed.ok) throw new GlobError(pattern, parsed.error);\n return parsed.glob;\n });\n return (target: string): boolean => {\n const value = target.trim();\n return globs.some((glob) => glob.match(value));\n };\n}\n\n/**\n * The literal directory a pattern's matches all sit under, or `''` for the root.\n *\n * `{docs,specs}/**` has two, and this is the directory they share; the walk\n * itself starts at each.\n */\nexport function globBase(pattern: string): string {\n const { glob } = pathList([pattern]).entries[0] as { readonly glob: Glob };\n const bases = glob.bases.map((base) => base.split('/'));\n const shared = bases[0] as string[];\n let length = shared.length;\n for (const base of bases) {\n let same = 0;\n // `<=` would read one past the end, where both sides are undefined and\n // equal, and slice the same prefix: an equivalent mutant, left untested.\n while (same < length && base[same] === shared[same]) same += 1;\n length = same;\n }\n return shared.slice(0, length).join('/');\n}\n\n/**\n * Whether a base names directories that exist with exactly that spelling.\n *\n * A walk starts where a pattern's literal prefix points rather than at the\n * root, and on a filesystem that ignores case, `readdir('Docs')` lists `docs`.\n * Every file under it would come back spelled `Docs/...` - a node id, a\n * baseline key - and `Docs/**` would find it on Windows and macOS and nothing\n * on Linux. Asking each parent for the name gives git's answer on every host. A\n * base with an empty segment is rooted at `/`, which is never inside the root.\n */\nasync function spelledAsOnDisk(root: string, base: string): Promise<boolean> {\n if (base.length === 0) return true;\n let directory = root;\n for (const segment of base.split('/')) {\n let names: string[];\n try {\n names = await readdir(directory);\n } catch {\n // Answering `true` here would change nothing a test can see: the walk\n // would then read a directory beneath this one, fail the same way, and\n // find nothing.\n return false;\n }\n if (!names.includes(segment)) return false;\n directory = `${directory}/${segment}`;\n }\n return true;\n}\n\n/**\n * Walks the tree and returns every file matching the patterns.\n *\n * Directories are pruned as early as possible: a repository with a 2 GB\n * `node_modules` should cost nothing to scan.\n */\nexport async function walkFiles(options: WalkOptions): Promise<WalkedFile[]> {\n const root = toPosix(options.root).replace(/\\/+$/, '');\n const patterns = pathList(options.patterns);\n const maxSize = options.maxFileSize ?? MAX_FILE_SIZE;\n\n // Ignores come in two shapes and both are documented. A bare name prunes any\n // directory called that, at any depth, the way a `.gitignore` line does.\n // Anything carrying a separator or glob syntax is matched against the\n // repository-relative path, which is what `--ignore \"docs/drafts/**\"` means -\n // and treating that as a directory name silently excluded nothing at all.\n const ignores = options.ignore ?? [];\n // The empty pattern is no directory's name, so it goes to the dialect, which\n // refuses it as it refuses an empty include: a pattern that names nothing is\n // a typo or an unset variable, and was an ignore that ignored nothing.\n const bare = (pattern: string): boolean => pattern.length > 0 && !isGlob(pattern) && !pattern.includes('/');\n const ignoredNames = new Set([...DEFAULT_IGNORED_DIRECTORIES, ...ignores.filter(bare)]);\n const ignoreList = pathList(ignores.filter((pattern) => !bare(pattern)));\n const excluded = (path: string): boolean => ignoreList.match(path);\n\n // Only walk the directories the patterns can possibly reach: each pattern's\n // literal prefix, one per brace alternative. A negated pattern adds none,\n // and adding its would change no answer - whatever lies beneath it that no\n // other pattern reaches, no other pattern matches either - so that mutant is\n // equivalent. No positive pattern at all walks nothing, which is the answer.\n const bases = new Set<string>();\n for (const entry of patterns.entries) {\n if (entry.negated) continue;\n for (const base of entry.glob.bases) bases.add(base);\n }\n // Drop any base already contained in another: walking it again would only\n // repeat work. The empty base is the repository root, which contains\n // everything, so when it is present it is the only root worth walking.\n const roots = bases.has('')\n ? ['']\n : [...bases].filter((base) => ![...bases].some((other) => other !== base && base.startsWith(`${other}/`)));\n\n const out = new Map<string, WalkedFile>();\n const visited = new Set<string>();\n\n const walk = async (relative: string): Promise<void> => {\n const absolute = relative.length === 0 ? root : `${root}/${relative}`;\n if (visited.has(absolute)) return;\n visited.add(absolute);\n\n let entries;\n try {\n entries = await readdir(absolute, { withFileTypes: true });\n } catch {\n // An unreadable directory is not a reason to fail the whole run.\n return;\n }\n\n for (const entry of entries) {\n const child = relative.length === 0 ? entry.name : `${relative}/${entry.name}`;\n\n if (entry.isDirectory()) {\n // Pruning a whole subtree is an optimisation; a pattern that only\n // matches the files inside it is still honoured when they are filtered.\n if (ignoredNames.has(entry.name) || excluded(child)) continue;\n await walk(child);\n continue;\n }\n\n if (entry.isSymbolicLink()) {\n if (!options.followSymlinks) continue;\n try {\n const info = await stat(`${root}/${child}`);\n if (info.isDirectory()) {\n await walk(child);\n continue;\n }\n if (!patterns.match(child) || excluded(child) || info.size > maxSize) continue;\n out.set(child, { path: child, absolute: `${root}/${child}`, size: info.size });\n } catch {\n continue;\n }\n continue;\n }\n\n if (!entry.isFile()) continue;\n if (!patterns.match(child) || excluded(child)) continue;\n\n try {\n const info = await stat(`${root}/${child}`);\n if (info.size > maxSize) continue;\n out.set(child, { path: child, absolute: `${root}/${child}`, size: info.size });\n } catch {\n continue;\n }\n }\n };\n\n for (const base of roots) if (await spelledAsOnDisk(root, base)) await walk(base);\n\n return [...out.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n}\n\n/**\n * Resolves a repository-relative path against the root, in POSIX form.\n *\n * An absolute path already names one place, and joining it under the root names\n * another: `--baseline /tmp/b.json` read `<root>/tmp/b.json`, found nothing\n * there, and accepted nothing without saying so.\n */\nexport function underRoot(root: string, relative: string): string {\n if (isAbsolutePath(relative)) return toPosix(relative);\n return joinPosix(toPosix(root), relative);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pattern matching and directory walking.\n *\n * The patterns are the `path` dialect every spec-* tool reads, from spec-core:\n * `**`, `*`, `?`, `{a,b}`, `[abc]` and `[!a-z]`, matched against whole\n * repository-relative POSIX paths by an automaton that cannot backtrack, with\n * case respected on every host (ADR-0022). What stays here is spec-graph's own:\n * how a list with `!` entries in it reads, which directories a walk never\n * enters, and how a reference target - which is not a path - is matched.\n */\n\nimport { readdir, stat } from 'node:fs/promises';\n\nimport { isAbsolutePath, joinPosix, toPosix } from './paths.js';\nimport {\n compileGlob as compileFamilyGlob,\n GlobError,\n isGlobSyntax,\n parseGlob,\n parseGlobList,\n type Glob,\n type GlobList,\n type GlobOptions as FamilyGlobOptions,\n type RegexMatcher,\n} from './vendor/spec-core/pattern/index.js';\n\n/** Directories skipped unless a pattern explicitly names them. */\nexport const DEFAULT_IGNORED_DIRECTORIES: readonly string[] = Object.freeze([\n '.git',\n '.hg',\n '.svn',\n '.cache',\n '.next',\n '.nuxt',\n '.turbo',\n '.venv',\n 'node_modules',\n 'bower_components',\n 'vendor',\n 'dist',\n 'build',\n 'out',\n 'target',\n 'coverage',\n '__pycache__',\n]);\n\n/** Files larger than this are skipped: a specification is not a binary. */\nexport const MAX_FILE_SIZE = 4 * 1024 * 1024;\n\nexport interface WalkedFile {\n /** Repository-relative POSIX path. */\n readonly path: string;\n /** Absolute host path, for reading. */\n readonly absolute: string;\n readonly size: number;\n}\n\nexport interface WalkOptions {\n readonly root: string;\n readonly patterns: readonly string[];\n readonly ignore?: readonly string[] | undefined;\n readonly maxFileSize?: number | undefined;\n /** Follow directory symlinks. Off by default: cycles are real. */\n readonly followSymlinks?: boolean | undefined;\n}\n\n/**\n * How a path pattern is read everywhere in spec-graph.\n *\n * A literal names a file or a directory and everything beneath it, which is\n * what `--ignore docs/drafts` has always meant. A `\\` is a separator, because a\n * pattern typed on a Windows shell arrives with them and spec-graph has always\n * read them that way; it cannot also be an escape.\n */\nconst PATH: FamilyGlobOptions = { dialect: 'path', caseSensitive: true, literal: 'either', backslash: 'separator' };\n\n/** True when the string contains glob syntax rather than being a literal path. */\nexport function isGlob(pattern: string): boolean {\n return isGlobSyntax(pattern);\n}\n\n/**\n * Compiles a glob to an anchored regular expression, in the dialect spec-graph\n * read before it adopted spec-core's.\n *\n * @deprecated Nothing matches with this. It stays for callers who want a\n * `RegExp`, and as the record of the old reading that `tests/glob.test.ts`\n * holds every difference against, each one named in ADR-0022. It no longer\n * folds case on Windows alone: an answer must not depend on the host.\n */\nexport function globToRegExp(pattern: string): RegExp {\n return new RegExp(`^${globSource(pattern)}$`);\n}\n\nexport interface GlobOptions {\n /** Fold case. Off unless asked for, on every host. */\n readonly ignoreCase?: boolean | undefined;\n}\n\n/**\n * Compiles one glob to a matcher over whole repository-relative paths.\n *\n * A pattern with no glob syntax names that path and nothing beneath it; a bare\n * directory meaning its contents is a reading of {@link createGlobMatcher}'s.\n * Throws a `GlobError` that names the glob as it was written.\n *\n * Matching is linear in the path whatever the pattern. It was not always: a\n * glob's stars are not nested, but each is a `[^/]*` to `RegExp`, and a subject\n * that fails is divided between them every way there is - `*-*-*-x` took two\n * minutes over a 10,000-character reference target (ADR-0017).\n */\nexport function compileGlob(pattern: string, options: GlobOptions = {}): RegexMatcher {\n // `literal: 'file'` is the dialect's reading of any value but `directory`\n // and `either`, so a mutant that blanks it reads the same. It is spelled out\n // because what an unknown value means is spec-core's to change.\n const glob = compileFamilyGlob(pattern, { ...PATH, literal: 'file', caseSensitive: options.ignoreCase !== true });\n return { test: (path) => glob.match(path), size: glob.automaton.kinds.length };\n}\n\n/** The expression a glob stood for before 0.9.0, before anchors and flags. */\nfunction globSource(pattern: string): string {\n let source = '';\n let i = 0;\n const braces: number[] = [];\n\n while (i < pattern.length) {\n const ch = pattern[i] as string;\n\n if (ch === '*') {\n const doubled = pattern[i + 1] === '*';\n if (doubled) {\n const slashAfter = pattern[i + 2] === '/';\n // `a/**/b` must also match `a/b`, so the separator is folded in.\n source += slashAfter ? '(?:.*/)?' : '.*';\n i += slashAfter ? 3 : 2;\n } else {\n source += '[^/]*';\n i += 1;\n }\n continue;\n }\n\n if (ch === '?') {\n source += '[^/]';\n i += 1;\n continue;\n }\n\n if (ch === '[') {\n const close = pattern.indexOf(']', i + 1);\n if (close === -1) {\n source += '\\\\[';\n i += 1;\n continue;\n }\n const body = pattern.slice(i + 1, close);\n source += `[${body.startsWith('!') ? `^${escapeClass(body.slice(1))}` : escapeClass(body)}]`;\n i = close + 1;\n continue;\n }\n\n if (ch === '{') {\n braces.push(source.length);\n source += '(?:';\n i += 1;\n continue;\n }\n if (ch === '}' && braces.length > 0) {\n braces.pop();\n source += ')';\n i += 1;\n continue;\n }\n if (ch === ',' && braces.length > 0) {\n source += '|';\n i += 1;\n continue;\n }\n\n source += ch.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&');\n i += 1;\n }\n\n // Otherwise reported as an unmatched parenthesis, in a glob that has none.\n if (braces.length > 0) throw new Error(`invalid glob \"${pattern}\": unclosed \"{\"`);\n return source;\n}\n\nfunction escapeClass(body: string): string {\n return body.replace(/[\\\\^\\]]/g, '\\\\$&');\n}\n\nexport interface GlobMatcher {\n (path: string): boolean;\n}\n\n/** Compiles a list of path patterns, or throws naming the one that is not a glob. */\nfunction pathList(patterns: readonly string[]): GlobList {\n const parsed = parseGlobList(patterns, PATH);\n if (!parsed.ok) throw new Error(`invalid glob ${parsed.error}`);\n return parsed.list;\n}\n\n/**\n * Builds a matcher from include patterns, honouring `!` negations.\n *\n * Later patterns win, so `docs/**\\/*.md` followed by `!docs/drafts/**` reads the\n * way a `.gitignore` does. A pattern with no glob syntax in it names a file, or\n * a directory and everything beneath it; `docs/` names only what is beneath.\n */\nexport function createGlobMatcher(patterns: readonly string[]): GlobMatcher {\n const list = pathList(patterns);\n return (path: string): boolean => list.match(path);\n}\n\n/**\n * Builds a predicate over reference *targets*, not paths.\n *\n * Separate from {@link createGlobMatcher} on purpose. That one is\n * path-oriented: a bare name there is a directory and everything under it. A\n * reference target has no such structure - `trap 55` is a name, not a location\n * - so a bare pattern matches it literally and nothing else.\n *\n * Case is ignored, on every platform, because a citation's spelling is the\n * author's and not the filesystem's. The fold is simple case mapping, one code\n * point at a time and without a locale, so every host gives the same answer:\n * U+00B5 (micro sign) and U+03BC (mu) stay two targets, as they have been since\n * a Windows-only `i` flag made them one there (ADR-0017).\n *\n * A target is text a document holds rather than a path on this host, and two\n * things follow. A `\\` escapes the character after it: there is no Windows\n * separator to allow for. And a `.` or `..` segment is text: in a path the\n * dialect drops the one and refuses the other as climbing out of the root, but\n * `../../notes/gone.md` is a link somebody wrote and may want left alone.\n */\nexport function createReferenceFilter(patterns: readonly string[]): (target: string) => boolean {\n // A blank pattern reaches the dialect, which refuses it, as every pattern\n // spec-graph reads is refused when it names nothing. It used to be dropped,\n // so an unset variable in `--ignore-ref \"$TAGS\"` went unnoticed.\n const globs: Glob[] = patterns.map((pattern) => {\n const literalDots = pattern\n .trim()\n .split('/')\n .map((segment) => (segment === '.' ? '\\\\.' : segment === '..' ? '\\\\.\\\\.' : segment))\n .join('/');\n // Every option spelled out, though the dialect reads `{}` the same way\n // today: its defaults are spec-core's to change, and this reading is\n // spec-graph's to keep. A mutant that drops one is equivalent until then.\n const parsed = parseGlob(literalDots, { dialect: 'path', caseSensitive: false, literal: 'file' });\n if (!parsed.ok) throw new GlobError(pattern, parsed.error);\n return parsed.glob;\n });\n return (target: string): boolean => {\n const value = target.trim();\n return globs.some((glob) => glob.match(value));\n };\n}\n\n/**\n * The literal directory a pattern's matches all sit under, or `''` for the root.\n *\n * `{docs,specs}/**` has two, and this is the directory they share; the walk\n * itself starts at each.\n */\nexport function globBase(pattern: string): string {\n const { glob } = pathList([pattern]).entries[0] as { readonly glob: Glob };\n const bases = glob.bases.map((base) => base.split('/'));\n const shared = bases[0] as string[];\n let length = shared.length;\n for (const base of bases) {\n let same = 0;\n // `<=` would read one past the end, where both sides are undefined and\n // equal, and slice the same prefix: an equivalent mutant, left untested.\n while (same < length && base[same] === shared[same]) same += 1;\n length = same;\n }\n return shared.slice(0, length).join('/');\n}\n\n/**\n * Whether a base names directories that exist with exactly that spelling.\n *\n * A walk starts where a pattern's literal prefix points rather than at the\n * root, and on a filesystem that ignores case, `readdir('Docs')` lists `docs`.\n * Every file under it would come back spelled `Docs/...` - a node id, a\n * baseline key - and `Docs/**` would find it on Windows and macOS and nothing\n * on Linux. Asking each parent for the name gives git's answer on every host. A\n * base with an empty segment is rooted at `/`, which is never inside the root.\n */\nasync function spelledAsOnDisk(root: string, base: string): Promise<boolean> {\n if (base.length === 0) return true;\n let directory = root;\n for (const segment of base.split('/')) {\n let names: string[];\n try {\n names = await readdir(directory);\n } catch {\n // Answering `true` here would change nothing a test can see: the walk\n // would then read a directory beneath this one, fail the same way, and\n // find nothing.\n return false;\n }\n if (!names.includes(segment)) return false;\n directory = `${directory}/${segment}`;\n }\n return true;\n}\n\n/**\n * Walks the tree and returns every file matching the patterns.\n *\n * Directories are pruned as early as possible: a repository with a 2 GB\n * `node_modules` should cost nothing to scan.\n */\nexport async function walkFiles(options: WalkOptions): Promise<WalkedFile[]> {\n const root = toPosix(options.root).replace(/\\/+$/, '');\n const patterns = pathList(options.patterns);\n const maxSize = options.maxFileSize ?? MAX_FILE_SIZE;\n\n // Ignores come in two shapes and both are documented. A bare name prunes any\n // directory called that, at any depth, the way a `.gitignore` line does.\n // Anything carrying a separator or glob syntax is matched against the\n // repository-relative path, which is what `--ignore \"docs/drafts/**\"` means -\n // and treating that as a directory name silently excluded nothing at all.\n // A mutant that defaults this to `['Stryker was here']` prunes directories of\n // that name, which no repository has: equivalent, and left untested.\n const ignores = options.ignore ?? [];\n // The empty pattern is no directory's name, so it goes to the dialect, which\n // refuses it as it refuses an empty include: a pattern that names nothing is\n // a typo or an unset variable, and was an ignore that ignored nothing.\n const bare = (pattern: string): boolean => pattern.length > 0 && !isGlob(pattern) && !pattern.includes('/');\n const named = new Set(ignores.filter(bare));\n const ignoredNames = new Set([...DEFAULT_IGNORED_DIRECTORIES, ...named]);\n const ignoreList = pathList(ignores.filter((pattern) => !bare(pattern)));\n const excluded = (path: string): boolean => ignoreList.match(path);\n\n // Only walk the directories the patterns can possibly reach: each pattern's\n // literal prefix, one per brace alternative. A negated pattern adds none: it\n // takes files back, and starting a walk at its prefix would enter what the\n // walk skips - `!node_modules/**/*.txt` beside `**/*.md` would read every\n // Markdown file in `node_modules`. No positive pattern at all walks nothing,\n // which is the answer.\n const bases = new Set<string>();\n for (const entry of patterns.entries) {\n if (entry.negated) continue;\n for (const base of entry.glob.bases) bases.add(base);\n }\n // Every base is walked, even one inside another, because the outer walk\n // prunes `vendor` and the rest of the default list on its way down, and a\n // pattern that starts inside one of those is read from there. Dropping the\n // inner base as already covered meant `docs/**/*.md` beside\n // `docs/vendor/specs/*.md` found less than the second pattern alone. A bare\n // `--ignore` name is not overruled that way: it is the user's, and prunes at\n // any depth, a pattern's own starting point included - so `--ignore adr`\n // still takes out `adr/`, which the default `adr/**/*.md` starts inside.\n const startable = (base: string): boolean => !base.split('/').some((segment) => named.has(segment));\n const out = new Map<string, WalkedFile>();\n // Where two walks overlap, whichever reaches a directory first reads it and\n // the other stops there. Reading it twice would find the same files under\n // the same keys, so the two mutants that drop this are equivalent.\n const visited = new Set<string>();\n\n const walk = async (relative: string): Promise<void> => {\n const absolute = relative.length === 0 ? root : `${root}/${relative}`;\n if (visited.has(absolute)) return;\n visited.add(absolute);\n\n let entries;\n try {\n entries = await readdir(absolute, { withFileTypes: true });\n } catch {\n // An unreadable directory is not a reason to fail the whole run.\n return;\n }\n\n for (const entry of entries) {\n const child = relative.length === 0 ? entry.name : `${relative}/${entry.name}`;\n\n if (entry.isDirectory()) {\n // Pruning a whole subtree is an optimisation; a pattern that only\n // matches the files inside it is still honoured when they are filtered.\n if (ignoredNames.has(entry.name) || excluded(child)) continue;\n await walk(child);\n continue;\n }\n\n if (entry.isSymbolicLink()) {\n if (!options.followSymlinks) continue;\n try {\n const info = await stat(`${root}/${child}`);\n if (info.isDirectory()) {\n await walk(child);\n continue;\n }\n if (!patterns.match(child) || excluded(child) || info.size > maxSize) continue;\n out.set(child, { path: child, absolute: `${root}/${child}`, size: info.size });\n } catch {\n // A link to nothing has nothing to read.\n }\n continue;\n }\n\n if (!entry.isFile()) continue;\n if (!patterns.match(child) || excluded(child)) continue;\n\n try {\n const info = await stat(`${root}/${child}`);\n if (info.size > maxSize) continue;\n out.set(child, { path: child, absolute: `${root}/${child}`, size: info.size });\n } catch {\n // Gone between the listing and the look: there is nothing to read.\n }\n }\n };\n\n for (const base of bases) if (startable(base) && (await spelledAsOnDisk(root, base))) await walk(base);\n\n // No two paths are equal, since they are the keys of `out`, so the order\n // needs only `<`, and `<=` would read the same.\n return [...out.values()].sort((a, b) => (a.path < b.path ? -1 : 1));\n}\n\n/**\n * Resolves a repository-relative path against the root, in POSIX form.\n *\n * An absolute path already names one place, and joining it under the root names\n * another: `--baseline /tmp/b.json` read `<root>/tmp/b.json`, found nothing\n * there, and accepted nothing without saying so.\n */\nexport function underRoot(root: string, relative: string): string {\n if (isAbsolutePath(relative)) return toPosix(relative);\n return joinPosix(toPosix(root), relative);\n}\n"]}
|
package/dist/graph.d.ts
CHANGED
|
@@ -32,7 +32,8 @@ export interface SpecGraph {
|
|
|
32
32
|
reach(id: string, kinds: readonly EdgeKind[]): ReadonlyMap<string, readonly Edge[]>;
|
|
33
33
|
/**
|
|
34
34
|
* Strongly connected components with more than one member, over the given
|
|
35
|
-
* edge kinds
|
|
35
|
+
* edge kinds, and each node with an edge to itself as a component of one.
|
|
36
|
+
* Each is a real cycle.
|
|
36
37
|
*
|
|
37
38
|
* With `byDocument`, relations are projected onto the documents that own
|
|
38
39
|
* their endpoints before the search. That is what catches two documents
|
package/dist/graph.js
CHANGED
|
@@ -55,6 +55,9 @@ export function buildGraph(input) {
|
|
|
55
55
|
return undefined;
|
|
56
56
|
if (found.kind === 'document')
|
|
57
57
|
return found;
|
|
58
|
+
// An item's document is a document, or is missing from a graph built
|
|
59
|
+
// from some documents and not others - never an item. So the check only
|
|
60
|
+
// narrows the type, and its mutant is equivalent.
|
|
58
61
|
const owner = nodes.get(found.document);
|
|
59
62
|
return owner?.kind === 'document' ? owner : undefined;
|
|
60
63
|
},
|
|
@@ -88,7 +91,7 @@ export function buildGraph(input) {
|
|
|
88
91
|
cycles(kinds, options) {
|
|
89
92
|
const relevant = (id) => [...filter(outgoing.get(id), kinds)];
|
|
90
93
|
if (!options?.byDocument) {
|
|
91
|
-
return tarjan([...nodes.keys()], (id) => relevant(id).map((edge) => edge.to)
|
|
94
|
+
return tarjan([...nodes.keys()], (id) => relevant(id).map((edge) => edge.to));
|
|
92
95
|
}
|
|
93
96
|
const ownerOf = (id) => graph.owningDocument(id)?.id ?? id;
|
|
94
97
|
const projected = new Map();
|
|
@@ -136,6 +139,9 @@ function tarjan(ids, successors) {
|
|
|
136
139
|
const indexOf = new Map();
|
|
137
140
|
const lowLink = new Map();
|
|
138
141
|
const onStack = new Set();
|
|
142
|
+
// A component is popped down to its own root and no further, so nothing
|
|
143
|
+
// beneath the first root pushed is ever popped: a stack that starts with
|
|
144
|
+
// something in it is an equivalent mutant.
|
|
139
145
|
const stack = [];
|
|
140
146
|
const components = [];
|
|
141
147
|
let counter = 0;
|
|
@@ -152,6 +158,9 @@ function tarjan(ids, successors) {
|
|
|
152
158
|
onStack.add(root);
|
|
153
159
|
while (work.length > 0) {
|
|
154
160
|
const frame = work[work.length - 1];
|
|
161
|
+
// `<=` reads one child past the end: `undefined`, a node with no
|
|
162
|
+
// successors that is settled on its own at once and is in no cycle. That
|
|
163
|
+
// mutant is equivalent.
|
|
155
164
|
if (frame.next < frame.children.length) {
|
|
156
165
|
const child = frame.children[frame.next];
|
|
157
166
|
frame.next += 1;
|
package/dist/graph.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The specification graph.\n *\n * An immutable, fully indexed view over the resolved corpus. Construction pays\n * for adjacency in both directions once so that every rule, query and report is\n * a lookup rather than a scan - the difference between a linter that runs on\n * every save and one that gets moved to a nightly job and then ignored.\n *\n * The algorithms here are the ones the rules genuinely need and no more:\n * adjacency, reachability, and Tarjan's strongly connected components for\n * finding circular delegations.\n */\n\nimport type { DocumentNode, Edge, EdgeKind, ItemNode, SpecNode } from './types.js';\nimport { EDGE_TRAITS } from './types.js';\n\nexport interface SpecGraph {\n readonly nodes: ReadonlyMap<string, SpecNode>;\n readonly edges: readonly Edge[];\n readonly documents: readonly DocumentNode[];\n readonly items: readonly ItemNode[];\n\n node(id: string): SpecNode | undefined;\n document(id: string): DocumentNode | undefined;\n /** The document a node belongs to: itself, or the item's owner. */\n owningDocument(id: string): DocumentNode | undefined;\n itemsOf(documentId: string): readonly ItemNode[];\n\n /** Edges leaving `id`, optionally filtered by kind. */\n out(id: string, kinds?: readonly EdgeKind[]): readonly Edge[];\n /** Edges entering `id`, optionally filtered by kind. */\n in(id: string, kinds?: readonly EdgeKind[]): readonly Edge[];\n\n /**\n * Every node reachable from `id` over the given edge kinds, with the shortest\n * path to each. `id` itself is included only when a cycle returns to it.\n */\n reach(id: string, kinds: readonly EdgeKind[]): ReadonlyMap<string, readonly Edge[]>;\n\n /**\n * Strongly connected components with more than one member, over the given\n * edge kinds. Each is a real cycle.\n *\n * With `byDocument`, relations are projected onto the documents that own\n * their endpoints before the search. That is what catches two documents\n * ping-ponging one obligation: the delegation runs item -> document in each\n * direction, so there is no cycle among the raw nodes even though the two\n * documents are plainly passing the same question back and forth.\n */\n cycles(kinds: readonly EdgeKind[], options?: { byDocument?: boolean }): readonly (readonly string[])[];\n}\n\n/** Builds the graph. Edges naming an unknown node are dropped, not stored. */\nexport function buildGraph(input: {\n readonly nodes: ReadonlyMap<string, SpecNode>;\n readonly edges: readonly Edge[];\n}): SpecGraph {\n const nodes = input.nodes;\n const edges = input.edges.filter((edge) => nodes.has(edge.from) && nodes.has(edge.to));\n\n const outgoing = new Map<string, Edge[]>();\n const incoming = new Map<string, Edge[]>();\n for (const edge of edges) {\n push(outgoing, edge.from, edge);\n push(incoming, edge.to, edge);\n }\n\n const documents: DocumentNode[] = [];\n const items: ItemNode[] = [];\n const itemsByDocument = new Map<string, ItemNode[]>();\n for (const node of nodes.values()) {\n if (node.kind === 'document') documents.push(node);\n else {\n items.push(node);\n push(itemsByDocument, node.document, node);\n }\n }\n\n const filter = (list: readonly Edge[] | undefined, kinds?: readonly EdgeKind[]): readonly Edge[] => {\n if (!list) return EMPTY_EDGES;\n if (!kinds || kinds.length === 0) return list;\n return list.filter((edge) => kinds.includes(edge.kind));\n };\n\n const graph: SpecGraph = {\n nodes,\n edges,\n documents,\n items,\n\n node: (id) => nodes.get(id),\n document: (id) => {\n const found = nodes.get(id);\n return found?.kind === 'document' ? found : undefined;\n },\n owningDocument: (id) => {\n const found = nodes.get(id);\n if (!found) return undefined;\n if (found.kind === 'document') return found;\n const owner = nodes.get(found.document);\n return owner?.kind === 'document' ? owner : undefined;\n },\n itemsOf: (documentId) => itemsByDocument.get(documentId) ?? EMPTY_ITEMS,\n\n out: (id, kinds) => filter(outgoing.get(id), kinds),\n in: (id, kinds) => filter(incoming.get(id), kinds),\n\n reach(id, kinds) {\n const paths = new Map<string, readonly Edge[]>();\n const queue: string[] = [id];\n const pathTo = new Map<string, readonly Edge[]>([[id, EMPTY_EDGES]]);\n\n while (queue.length > 0) {\n const current = queue.shift() as string;\n const prefix = pathTo.get(current) as readonly Edge[];\n for (const edge of filter(outgoing.get(current), kinds)) {\n if (pathTo.has(edge.to) && edge.to !== id) continue;\n const path = [...prefix, edge];\n if (edge.to === id) {\n // A cycle back to the origin: record it, but do not re-expand.\n if (!paths.has(id)) paths.set(id, path);\n continue;\n }\n pathTo.set(edge.to, path);\n paths.set(edge.to, path);\n queue.push(edge.to);\n }\n }\n return paths;\n },\n\n cycles(kinds, options) {\n const relevant = (id: string): Edge[] => [...filter(outgoing.get(id), kinds)];\n if (!options?.byDocument) {\n return tarjan([...nodes.keys()], (id) => relevant(id).map((edge) => edge.to).filter((to) => nodes.has(to)));\n }\n const ownerOf = (id: string): string => graph.owningDocument(id)?.id ?? id;\n const projected = new Map<string, Set<string>>();\n for (const node of nodes.values()) {\n for (const edge of relevant(node.id)) {\n const from = ownerOf(edge.from);\n const to = ownerOf(edge.to);\n // A relation that stays inside one document is a self-reference, not\n // a cycle, and has a rule of its own.\n if (from === to) continue;\n const set = projected.get(from) ?? new Set<string>();\n set.add(to);\n projected.set(from, set);\n }\n }\n return tarjan(\n documents.map((document) => document.id),\n (id) => [...(projected.get(id) ?? [])],\n );\n },\n };\n\n return graph;\n}\n\nconst EMPTY_EDGES: readonly Edge[] = Object.freeze([]);\nconst EMPTY_ITEMS: readonly ItemNode[] = Object.freeze([]);\n\nfunction push<T>(map: Map<string, T[]>, key: string, value: T): void {\n const list = map.get(key);\n if (list) list.push(value);\n else map.set(key, [value]);\n}\n\n/* -------------------------------------------------------------------------- */\n/* Cycles */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Tarjan's strongly connected components, iterative.\n *\n * Iterative rather than recursive because a monorepo's reference graph can be\n * deep enough to blow the call stack, and a linter that crashes on the largest\n * repository in the company is a linter nobody trusts on the small ones either.\n *\n * Takes a successor function so the same search serves both the raw graph and\n * the document-level projection.\n */\nfunction tarjan(ids: readonly string[], successors: (id: string) => string[]): string[][] {\n const indexOf = new Map<string, number>();\n const lowLink = new Map<string, number>();\n const onStack = new Set<string>();\n const stack: string[] = [];\n const components: string[][] = [];\n let counter = 0;\n\n for (const root of ids) {\n if (indexOf.has(root)) continue;\n\n const work: { id: string; next: number; children: string[] }[] = [\n { id: root, next: 0, children: successors(root) },\n ];\n indexOf.set(root, counter);\n lowLink.set(root, counter);\n counter += 1;\n stack.push(root);\n onStack.add(root);\n\n while (work.length > 0) {\n const frame = work[work.length - 1] as { id: string; next: number; children: string[] };\n if (frame.next < frame.children.length) {\n const child = frame.children[frame.next] as string;\n frame.next += 1;\n if (!indexOf.has(child)) {\n indexOf.set(child, counter);\n lowLink.set(child, counter);\n counter += 1;\n stack.push(child);\n onStack.add(child);\n work.push({ id: child, next: 0, children: successors(child) });\n } else if (onStack.has(child)) {\n lowLink.set(frame.id, Math.min(lowLink.get(frame.id) as number, indexOf.get(child) as number));\n }\n continue;\n }\n\n work.pop();\n const parent = work[work.length - 1];\n if (parent) {\n lowLink.set(parent.id, Math.min(lowLink.get(parent.id) as number, lowLink.get(frame.id) as number));\n }\n\n if (lowLink.get(frame.id) === indexOf.get(frame.id)) {\n const component: string[] = [];\n for (;;) {\n const popped = stack.pop() as string;\n onStack.delete(popped);\n component.push(popped);\n if (popped === frame.id) break;\n }\n if (component.length > 1) components.push(component.reverse());\n else if (successors(frame.id).includes(frame.id)) components.push(component);\n }\n }\n }\n\n return components;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Derived predicates */\n/* -------------------------------------------------------------------------- */\n\n/** Edge kinds that move an obligation from one document to another. */\nexport const OBLIGATION_EDGES: readonly EdgeKind[] = Object.freeze(\n (Object.keys(EDGE_TRAITS) as EdgeKind[]).filter((kind) => EDGE_TRAITS[kind].transfersObligation),\n);\n\n/** Edge kinds whose source depends on what the target says. */\nexport const LOAD_BEARING_EDGES: readonly EdgeKind[] = Object.freeze(\n (Object.keys(EDGE_TRAITS) as EdgeKind[]).filter((kind) => EDGE_TRAITS[kind].loadBearing),\n);\n"]}
|
|
1
|
+
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The specification graph.\n *\n * An immutable, fully indexed view over the resolved corpus. Construction pays\n * for adjacency in both directions once so that every rule, query and report is\n * a lookup rather than a scan - the difference between a linter that runs on\n * every save and one that gets moved to a nightly job and then ignored.\n *\n * The algorithms here are the ones the rules genuinely need and no more:\n * adjacency, reachability, and Tarjan's strongly connected components for\n * finding circular delegations.\n */\n\nimport type { DocumentNode, Edge, EdgeKind, ItemNode, SpecNode } from './types.js';\nimport { EDGE_TRAITS } from './types.js';\n\nexport interface SpecGraph {\n readonly nodes: ReadonlyMap<string, SpecNode>;\n readonly edges: readonly Edge[];\n readonly documents: readonly DocumentNode[];\n readonly items: readonly ItemNode[];\n\n node(id: string): SpecNode | undefined;\n document(id: string): DocumentNode | undefined;\n /** The document a node belongs to: itself, or the item's owner. */\n owningDocument(id: string): DocumentNode | undefined;\n itemsOf(documentId: string): readonly ItemNode[];\n\n /** Edges leaving `id`, optionally filtered by kind. */\n out(id: string, kinds?: readonly EdgeKind[]): readonly Edge[];\n /** Edges entering `id`, optionally filtered by kind. */\n in(id: string, kinds?: readonly EdgeKind[]): readonly Edge[];\n\n /**\n * Every node reachable from `id` over the given edge kinds, with the shortest\n * path to each. `id` itself is included only when a cycle returns to it.\n */\n reach(id: string, kinds: readonly EdgeKind[]): ReadonlyMap<string, readonly Edge[]>;\n\n /**\n * Strongly connected components with more than one member, over the given\n * edge kinds, and each node with an edge to itself as a component of one.\n * Each is a real cycle.\n *\n * With `byDocument`, relations are projected onto the documents that own\n * their endpoints before the search. That is what catches two documents\n * ping-ponging one obligation: the delegation runs item -> document in each\n * direction, so there is no cycle among the raw nodes even though the two\n * documents are plainly passing the same question back and forth.\n */\n cycles(kinds: readonly EdgeKind[], options?: { byDocument?: boolean }): readonly (readonly string[])[];\n}\n\n/** Builds the graph. Edges naming an unknown node are dropped, not stored. */\nexport function buildGraph(input: {\n readonly nodes: ReadonlyMap<string, SpecNode>;\n readonly edges: readonly Edge[];\n}): SpecGraph {\n const nodes = input.nodes;\n const edges = input.edges.filter((edge) => nodes.has(edge.from) && nodes.has(edge.to));\n\n const outgoing = new Map<string, Edge[]>();\n const incoming = new Map<string, Edge[]>();\n for (const edge of edges) {\n push(outgoing, edge.from, edge);\n push(incoming, edge.to, edge);\n }\n\n const documents: DocumentNode[] = [];\n const items: ItemNode[] = [];\n const itemsByDocument = new Map<string, ItemNode[]>();\n for (const node of nodes.values()) {\n if (node.kind === 'document') documents.push(node);\n else {\n items.push(node);\n push(itemsByDocument, node.document, node);\n }\n }\n\n const filter = (list: readonly Edge[] | undefined, kinds?: readonly EdgeKind[]): readonly Edge[] => {\n if (!list) return EMPTY_EDGES;\n if (!kinds || kinds.length === 0) return list;\n return list.filter((edge) => kinds.includes(edge.kind));\n };\n\n const graph: SpecGraph = {\n nodes,\n edges,\n documents,\n items,\n\n node: (id) => nodes.get(id),\n document: (id) => {\n const found = nodes.get(id);\n return found?.kind === 'document' ? found : undefined;\n },\n owningDocument: (id) => {\n const found = nodes.get(id);\n if (!found) return undefined;\n if (found.kind === 'document') return found;\n // An item's document is a document, or is missing from a graph built\n // from some documents and not others - never an item. So the check only\n // narrows the type, and its mutant is equivalent.\n const owner = nodes.get(found.document);\n return owner?.kind === 'document' ? owner : undefined;\n },\n itemsOf: (documentId) => itemsByDocument.get(documentId) ?? EMPTY_ITEMS,\n\n out: (id, kinds) => filter(outgoing.get(id), kinds),\n in: (id, kinds) => filter(incoming.get(id), kinds),\n\n reach(id, kinds) {\n const paths = new Map<string, readonly Edge[]>();\n const queue: string[] = [id];\n const pathTo = new Map<string, readonly Edge[]>([[id, EMPTY_EDGES]]);\n\n while (queue.length > 0) {\n const current = queue.shift() as string;\n const prefix = pathTo.get(current) as readonly Edge[];\n for (const edge of filter(outgoing.get(current), kinds)) {\n if (pathTo.has(edge.to) && edge.to !== id) continue;\n const path = [...prefix, edge];\n if (edge.to === id) {\n // A cycle back to the origin: record it, but do not re-expand.\n if (!paths.has(id)) paths.set(id, path);\n continue;\n }\n pathTo.set(edge.to, path);\n paths.set(edge.to, path);\n queue.push(edge.to);\n }\n }\n return paths;\n },\n\n cycles(kinds, options) {\n const relevant = (id: string): Edge[] => [...filter(outgoing.get(id), kinds)];\n if (!options?.byDocument) {\n return tarjan([...nodes.keys()], (id) => relevant(id).map((edge) => edge.to));\n }\n const ownerOf = (id: string): string => graph.owningDocument(id)?.id ?? id;\n const projected = new Map<string, Set<string>>();\n for (const node of nodes.values()) {\n for (const edge of relevant(node.id)) {\n const from = ownerOf(edge.from);\n const to = ownerOf(edge.to);\n // A relation that stays inside one document is a self-reference, not\n // a cycle, and has a rule of its own.\n if (from === to) continue;\n const set = projected.get(from) ?? new Set<string>();\n set.add(to);\n projected.set(from, set);\n }\n }\n return tarjan(\n documents.map((document) => document.id),\n (id) => [...(projected.get(id) ?? [])],\n );\n },\n };\n\n return graph;\n}\n\nconst EMPTY_EDGES: readonly Edge[] = Object.freeze([]);\nconst EMPTY_ITEMS: readonly ItemNode[] = Object.freeze([]);\n\nfunction push<T>(map: Map<string, T[]>, key: string, value: T): void {\n const list = map.get(key);\n if (list) list.push(value);\n else map.set(key, [value]);\n}\n\n/* -------------------------------------------------------------------------- */\n/* Cycles */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Tarjan's strongly connected components, iterative.\n *\n * Iterative rather than recursive because a monorepo's reference graph can be\n * deep enough to blow the call stack, and a linter that crashes on the largest\n * repository in the company is a linter nobody trusts on the small ones either.\n *\n * Takes a successor function so the same search serves both the raw graph and\n * the document-level projection.\n */\nfunction tarjan(ids: readonly string[], successors: (id: string) => string[]): string[][] {\n const indexOf = new Map<string, number>();\n const lowLink = new Map<string, number>();\n const onStack = new Set<string>();\n // A component is popped down to its own root and no further, so nothing\n // beneath the first root pushed is ever popped: a stack that starts with\n // something in it is an equivalent mutant.\n const stack: string[] = [];\n const components: string[][] = [];\n let counter = 0;\n\n for (const root of ids) {\n if (indexOf.has(root)) continue;\n\n const work: { id: string; next: number; children: string[] }[] = [\n { id: root, next: 0, children: successors(root) },\n ];\n indexOf.set(root, counter);\n lowLink.set(root, counter);\n counter += 1;\n stack.push(root);\n onStack.add(root);\n\n while (work.length > 0) {\n const frame = work[work.length - 1] as { id: string; next: number; children: string[] };\n // `<=` reads one child past the end: `undefined`, a node with no\n // successors that is settled on its own at once and is in no cycle. That\n // mutant is equivalent.\n if (frame.next < frame.children.length) {\n const child = frame.children[frame.next] as string;\n frame.next += 1;\n if (!indexOf.has(child)) {\n indexOf.set(child, counter);\n lowLink.set(child, counter);\n counter += 1;\n stack.push(child);\n onStack.add(child);\n work.push({ id: child, next: 0, children: successors(child) });\n } else if (onStack.has(child)) {\n lowLink.set(frame.id, Math.min(lowLink.get(frame.id) as number, indexOf.get(child) as number));\n }\n continue;\n }\n\n work.pop();\n const parent = work[work.length - 1];\n if (parent) {\n lowLink.set(parent.id, Math.min(lowLink.get(parent.id) as number, lowLink.get(frame.id) as number));\n }\n\n if (lowLink.get(frame.id) === indexOf.get(frame.id)) {\n const component: string[] = [];\n for (;;) {\n const popped = stack.pop() as string;\n onStack.delete(popped);\n component.push(popped);\n if (popped === frame.id) break;\n }\n if (component.length > 1) components.push(component.reverse());\n else if (successors(frame.id).includes(frame.id)) components.push(component);\n }\n }\n }\n\n return components;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Derived predicates */\n/* -------------------------------------------------------------------------- */\n\n/** Edge kinds that move an obligation from one document to another. */\nexport const OBLIGATION_EDGES: readonly EdgeKind[] = Object.freeze(\n (Object.keys(EDGE_TRAITS) as EdgeKind[]).filter((kind) => EDGE_TRAITS[kind].transfersObligation),\n);\n\n/** Edge kinds whose source depends on what the target says. */\nexport const LOAD_BEARING_EDGES: readonly EdgeKind[] = Object.freeze(\n (Object.keys(EDGE_TRAITS) as EdgeKind[]).filter((kind) => EDGE_TRAITS[kind].loadBearing),\n);\n"]}
|
package/dist/identity.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface IdentityInput {
|
|
|
46
46
|
*/
|
|
47
47
|
readonly includePathAliases?: boolean | undefined;
|
|
48
48
|
}
|
|
49
|
+
/** An id has to be a name. Punctuation alone is what a bad parse leaves behind. */
|
|
50
|
+
export declare const HAS_LETTER_OR_DIGIT: RegExp;
|
|
49
51
|
/**
|
|
50
52
|
* Derives a document's canonical id and every alias that should reach it.
|
|
51
53
|
*
|
|
@@ -87,6 +89,10 @@ export declare function looksLikePath(target: string): boolean;
|
|
|
87
89
|
* is a different tool's job. Treating it as a dangling specification reference
|
|
88
90
|
* would report an error on one of the most ordinary things a design document
|
|
89
91
|
* does, so targets carrying a non-document extension are not references at all.
|
|
92
|
+
*
|
|
93
|
+
* A dotfile is such a target too - `../.nvmrc` names a file whose extension is
|
|
94
|
+
* its whole name - and so is a file whose extension is long: `arch.excalidraw`
|
|
95
|
+
* is a drawing, not an identifier with a dot in it (ADR-0006).
|
|
90
96
|
*/
|
|
91
97
|
export declare function isDocumentTarget(target: string): boolean;
|
|
92
98
|
/** True when a reference target points outside the repository. */
|
package/dist/identity.js
CHANGED
|
@@ -41,7 +41,7 @@ const PREFIXED_ID = /^([A-Za-z][A-Za-z0-9_]{0,15}?)[\s._-]*(\d{1,6})$/;
|
|
|
41
41
|
/** A bare number, optionally hash-prefixed: `7`, `#0007`. */
|
|
42
42
|
const BARE_NUMBER = /^#?(\d{1,6})$/;
|
|
43
43
|
/** An id has to be a name. Punctuation alone is what a bad parse leaves behind. */
|
|
44
|
-
const HAS_LETTER_OR_DIGIT = /[\p{L}\p{N}]/u;
|
|
44
|
+
export const HAS_LETTER_OR_DIGIT = /[\p{L}\p{N}]/u;
|
|
45
45
|
/**
|
|
46
46
|
* Derives a document's canonical id and every alias that should reach it.
|
|
47
47
|
*
|
|
@@ -60,7 +60,9 @@ export function identify(input) {
|
|
|
60
60
|
// worked example, parses its bare value as a lone backslash - and a document
|
|
61
61
|
// whose id is `\` collides with every other one that made the same mistake,
|
|
62
62
|
// exports as invalid Mermaid, and names itself in findings a reader cannot act
|
|
63
|
-
// on. There is nothing to gain by taking such a value seriously.
|
|
63
|
+
// on. There is nothing to gain by taking such a value seriously. The null
|
|
64
|
+
// check is for the type: `test(null)` reads the word "null" and passes, and
|
|
65
|
+
// the value it would keep is null anyway.
|
|
64
66
|
const declaredId = input.declaredId !== null && HAS_LETTER_OR_DIGIT.test(input.declaredId) ? input.declaredId : null;
|
|
65
67
|
// What the flag decides, named for what it means: the file's path and its
|
|
66
68
|
// name are one claim, and a region makes neither of them.
|
|
@@ -75,32 +77,24 @@ export function identify(input) {
|
|
|
75
77
|
if (key.length > 0)
|
|
76
78
|
aliases.add(key);
|
|
77
79
|
};
|
|
80
|
+
// Only a spelling that yields a number decides anything below, so only those
|
|
81
|
+
// are kept. `digits` is the number as it was written there.
|
|
78
82
|
const candidates = [];
|
|
79
83
|
const consider = (raw, allowBare) => {
|
|
80
84
|
if (!raw)
|
|
81
85
|
return;
|
|
82
86
|
const cleaned = raw.trim();
|
|
83
|
-
if (cleaned.length === 0)
|
|
84
|
-
return;
|
|
85
87
|
const prefixed = PREFIXED_ID.exec(cleaned);
|
|
86
88
|
if (prefixed) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
number: Number.parseInt(prefixed[2], 10),
|
|
90
|
-
label: cleaned,
|
|
91
|
-
});
|
|
89
|
+
const digits = prefixed[2];
|
|
90
|
+
candidates.push({ family: prefixed[1].toUpperCase(), number: Number.parseInt(digits, 10), digits });
|
|
92
91
|
return;
|
|
93
92
|
}
|
|
94
93
|
const bare = BARE_NUMBER.exec(cleaned);
|
|
95
94
|
if (bare && allowBare) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
number: Number.parseInt(bare[1], 10),
|
|
99
|
-
label: cleaned,
|
|
100
|
-
});
|
|
101
|
-
return;
|
|
95
|
+
const digits = bare[1];
|
|
96
|
+
candidates.push({ family: directoryFamily, number: Number.parseInt(digits, 10), digits });
|
|
102
97
|
}
|
|
103
|
-
candidates.push({ family: null, number: null, label: cleaned });
|
|
104
98
|
};
|
|
105
99
|
consider(declaredId, true);
|
|
106
100
|
// `0007-sharding-the-write-path.md` and `kep-1234-foo.md` both start with the
|
|
@@ -122,18 +116,23 @@ export function identify(input) {
|
|
|
122
116
|
// the two cases are one shape, `slug:` holds a URL segment rather than an id,
|
|
123
117
|
// and `slug: sharding` under `# ADR-0007` has to stay ADR-0007. See the fifth
|
|
124
118
|
// asymmetry in ADR-0004 for what that costs and why it is not guessed at.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
119
|
+
//
|
|
120
|
+
// Considered last, it names the document only where nothing before it gave
|
|
121
|
+
// a number: only the first candidate is read. A title that does name it
|
|
122
|
+
// supplies the number, and the spellings of that number registered below
|
|
123
|
+
// include the title's own. `false` decides nothing, since an identifier a
|
|
124
|
+
// heading opens with always starts with letters; it says a bare number in a
|
|
125
|
+
// title would name nothing.
|
|
126
|
+
consider(ownsFile ? headingId(input.heading) : null, false);
|
|
127
|
+
const numbered = candidates[0];
|
|
128
128
|
const family = numbered?.family ?? directoryFamily;
|
|
129
129
|
const number = numbered?.number ?? null;
|
|
130
|
-
// Witnesses to the local zero-padding convention, most authoritative first.
|
|
131
|
-
// The spelling that actually produced the number comes first: an id read from
|
|
132
|
-
// the H1 of `sharding.md` must still render as ADR-0007, not ADR-7.
|
|
133
|
-
const witnesses = [numbered?.label, declaredId, stem];
|
|
134
130
|
let id;
|
|
135
|
-
if (
|
|
136
|
-
|
|
131
|
+
if (numbered !== undefined && family !== null) {
|
|
132
|
+
// Written as the number was written where it was found, which is the local
|
|
133
|
+
// zero-padding convention: an id read from the H1 of `sharding.md` still
|
|
134
|
+
// renders as ADR-0007, not ADR-7, and one declared as `S3-0012` stays that.
|
|
135
|
+
id = `${family}-${numbered.digits}`;
|
|
137
136
|
}
|
|
138
137
|
else if (declaredId) {
|
|
139
138
|
id = declaredId.trim();
|
|
@@ -142,6 +141,10 @@ export function identify(input) {
|
|
|
142
141
|
// Numbered but with no family to prefix - a Rust RFC in `text/0001-foo.md`,
|
|
143
142
|
// say. The file stem is a far better identity than a bare "1": it is what
|
|
144
143
|
// people actually type, and it stays unique across the repository.
|
|
144
|
+
//
|
|
145
|
+
// A file's stem is never empty - `.md` is its own - so this fallback, and
|
|
146
|
+
// with it the difference between this branch and the next, is reached only
|
|
147
|
+
// by a caller identifying an empty path.
|
|
145
148
|
id = stem.length > 0 ? stem : String(number);
|
|
146
149
|
}
|
|
147
150
|
else {
|
|
@@ -156,9 +159,8 @@ export function identify(input) {
|
|
|
156
159
|
add(declaredId);
|
|
157
160
|
for (const alias of input.declaredAliases)
|
|
158
161
|
add(alias);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const spellings = numberSpellings(number, witnesses);
|
|
162
|
+
if (numbered !== undefined) {
|
|
163
|
+
const spellings = numberSpellings(numbered.number, numbered.digits);
|
|
162
164
|
if (family !== null) {
|
|
163
165
|
// Every spelling a human might type. `normaliseRef` folds separators, so
|
|
164
166
|
// `adr-7`, `adr 7` and `adr7` collapse to one key; the padding variants do
|
|
@@ -182,21 +184,20 @@ export function identify(input) {
|
|
|
182
184
|
* so `ADR-0007`, `adr 0007` and `Adr_0007` all become `adr0007`.
|
|
183
185
|
*/
|
|
184
186
|
export function normaliseRef(value) {
|
|
187
|
+
// The separators go last and take every space with them, so nothing is left
|
|
188
|
+
// to trim. `+` only saves work: each separator is removed either way.
|
|
185
189
|
return value
|
|
186
190
|
.trim()
|
|
187
191
|
.toLowerCase()
|
|
188
192
|
.replace(/^[#<([]+|[)\]>.,;:]+$/g, '')
|
|
189
|
-
.replace(/[\s._-]+/g, '')
|
|
190
|
-
.trim();
|
|
193
|
+
.replace(/[\s._-]+/g, '');
|
|
191
194
|
}
|
|
192
195
|
/** Splits a `target#anchor` reference. */
|
|
193
196
|
export function splitAnchor(target) {
|
|
194
197
|
const hash = target.indexOf('#');
|
|
195
198
|
if (hash === -1)
|
|
196
199
|
return { target, anchor: null };
|
|
197
|
-
// A leading `#`
|
|
198
|
-
if (hash === 0)
|
|
199
|
-
return { target: '', anchor: target.slice(1) };
|
|
200
|
+
// A leading `#` leaves the target empty: an anchor in the citing document.
|
|
200
201
|
return { target: target.slice(0, hash), anchor: target.slice(hash + 1) };
|
|
201
202
|
}
|
|
202
203
|
/** Parses `ADR-0007` into its family and number, or returns `null`. */
|
|
@@ -223,18 +224,27 @@ const DOCUMENT_EXTENSION = /\.(md|markdown|mdx|txt|rst|adoc|asciidoc)$/i;
|
|
|
223
224
|
* is a different tool's job. Treating it as a dangling specification reference
|
|
224
225
|
* would report an error on one of the most ordinary things a design document
|
|
225
226
|
* does, so targets carrying a non-document extension are not references at all.
|
|
227
|
+
*
|
|
228
|
+
* A dotfile is such a target too - `../.nvmrc` names a file whose extension is
|
|
229
|
+
* its whole name - and so is a file whose extension is long: `arch.excalidraw`
|
|
230
|
+
* is a drawing, not an identifier with a dot in it (ADR-0006).
|
|
226
231
|
*/
|
|
227
232
|
export function isDocumentTarget(target) {
|
|
228
233
|
const withoutAnchor = splitAnchor(target).target;
|
|
229
234
|
const base = withoutAnchor.slice(withoutAnchor.lastIndexOf('/') + 1);
|
|
230
235
|
const dot = base.lastIndexOf('.');
|
|
231
236
|
// No extension: a directory, or an extensionless spec such as `rfcs/0007`.
|
|
232
|
-
|
|
237
|
+
// Said outright rather than left to the test below, which would refuse the
|
|
238
|
+
// last character `slice(-1)` handed it and answer the same.
|
|
239
|
+
if (dot === -1)
|
|
233
240
|
return true;
|
|
234
241
|
const extension = base.slice(dot);
|
|
235
|
-
// Only a
|
|
236
|
-
// `v1.2` or `ADR-0007.1` keeps its dot and is still
|
|
237
|
-
|
|
242
|
+
// Only a file extension disqualifies a target, and one starts with a letter.
|
|
243
|
+
// An identifier like `v1.2` or `ADR-0007.1` keeps its dot and is still
|
|
244
|
+
// resolved as an id. The extension starts at the last dot and holds no
|
|
245
|
+
// other, so the leading anchor decides nothing; it says the dot is where the
|
|
246
|
+
// extension begins.
|
|
247
|
+
if (!/^\.[A-Za-z][A-Za-z0-9]*$/.test(extension))
|
|
238
248
|
return true;
|
|
239
249
|
return DOCUMENT_EXTENSION.test(base);
|
|
240
250
|
}
|
|
@@ -262,7 +272,8 @@ function fileStem(posixPath) {
|
|
|
262
272
|
const stem = dot > 0 ? base.slice(0, dot) : base;
|
|
263
273
|
// `docs/adr/0007-sharding/README.md` is identified by its directory - but a
|
|
264
274
|
// README at the repository root has no directory to be named after, and must
|
|
265
|
-
// keep its own name.
|
|
275
|
+
// keep its own name. A repository-relative path never starts with `/`, so
|
|
276
|
+
// `slash > 0` and `slash >= 0` agree on every path this is given.
|
|
266
277
|
const slash = posixPath.lastIndexOf('/');
|
|
267
278
|
if (slash > 0 && /^(readme|index)$/i.test(stem)) {
|
|
268
279
|
const parent = posixPath.slice(0, slash);
|
|
@@ -275,7 +286,13 @@ function stripExtension(posixPath) {
|
|
|
275
286
|
const slash = posixPath.lastIndexOf('/');
|
|
276
287
|
return dot > slash ? posixPath.slice(0, dot) : posixPath;
|
|
277
288
|
}
|
|
278
|
-
/**
|
|
289
|
+
/**
|
|
290
|
+
* `kep-1234-foo` -> `kep-1234`; `0007-sharding` -> `0007`; `sharding` -> `sharding`.
|
|
291
|
+
*
|
|
292
|
+
* The `$` alternatives decide nothing: a stem that is the identifier alone is
|
|
293
|
+
* handed back whole by the last line, and parses the same. They say that an
|
|
294
|
+
* identifier may end the name as well as open it.
|
|
295
|
+
*/
|
|
279
296
|
function leadingToken(stem) {
|
|
280
297
|
const prefixed = /^([A-Za-z]{1,15}[\s._-]?\d{1,6})(?:[\s._-]|$)/.exec(stem);
|
|
281
298
|
if (prefixed)
|
|
@@ -287,42 +304,20 @@ function leadingToken(stem) {
|
|
|
287
304
|
}
|
|
288
305
|
/** `# ADR-0007: Sharding the write path` -> `ADR-0007`. */
|
|
289
306
|
function headingId(heading) {
|
|
307
|
+
// Only saves the call: `exec` would read null as the word "null", and
|
|
308
|
+
// neither that nor an empty heading holds a number.
|
|
290
309
|
if (!heading)
|
|
291
310
|
return null;
|
|
292
311
|
const match = /^\s*([A-Za-z]{1,15}[\s._-]?\d{1,6})\b/.exec(heading);
|
|
293
312
|
return match ? match[1] : null;
|
|
294
313
|
}
|
|
295
314
|
/**
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
* A repository that writes `0007` everywhere should see `ADR-0007` in reports,
|
|
299
|
-
* not `ADR-7`. The file name is the most reliable witness of the local
|
|
300
|
-
* convention, so it decides.
|
|
315
|
+
* Zero-padded spellings a citation might reasonably use: the number as it was
|
|
316
|
+
* written, unpadded, and padded to three and to four digits.
|
|
301
317
|
*/
|
|
302
|
-
function
|
|
303
|
-
const
|
|
304
|
-
return
|
|
305
|
-
}
|
|
306
|
-
/** The digit width of the first witness that actually spells this number. */
|
|
307
|
-
function paddingWidth(number, witnesses) {
|
|
308
|
-
for (const witness of witnesses) {
|
|
309
|
-
if (!witness)
|
|
310
|
-
continue;
|
|
311
|
-
const digits = /(\d{1,6})/.exec(witness)?.[1];
|
|
312
|
-
if (digits && Number.parseInt(digits, 10) === number)
|
|
313
|
-
return digits.length;
|
|
314
|
-
}
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
317
|
-
/** Zero-padded spellings a citation might reasonably use. */
|
|
318
|
-
function numberSpellings(number, witnesses) {
|
|
319
|
-
const out = new Set([String(number)]);
|
|
320
|
-
const observed = paddingWidth(number, witnesses);
|
|
321
|
-
if (observed !== null)
|
|
322
|
-
out.add(String(number).padStart(observed, '0'));
|
|
323
|
-
for (const width of [3, 4])
|
|
324
|
-
out.add(String(number).padStart(width, '0'));
|
|
325
|
-
return [...out];
|
|
318
|
+
function numberSpellings(number, written) {
|
|
319
|
+
const unpadded = String(number);
|
|
320
|
+
return [...new Set([written, unpadded, unpadded.padStart(3, '0'), unpadded.padStart(4, '0')])];
|
|
326
321
|
}
|
|
327
322
|
/**
|
|
328
323
|
* True when two folded spellings are one edit apart.
|
|
@@ -334,6 +329,11 @@ function numberSpellings(number, witnesses) {
|
|
|
334
329
|
* more code, not less.
|
|
335
330
|
*/
|
|
336
331
|
export function withinOneEdit(a, b) {
|
|
332
|
+
// The loop bounds are not boundaries: one step past the end of equal-length
|
|
333
|
+
// strings compares undefined with undefined and continues, and one past the
|
|
334
|
+
// end of the shorter compares nothing with the last character of the longer
|
|
335
|
+
// and finds the two empty remainders equal - both the answer the loop's exit
|
|
336
|
+
// gives. Nor does `<` against `<=` below matter: equal lengths never get there.
|
|
337
337
|
if (a === b)
|
|
338
338
|
return false;
|
|
339
339
|
if (a.length === b.length) {
|