From befd7e70d14fda2db03853c9ec80d21fe701fa57 Mon Sep 17 00:00:00 2001 From: Ashwani Kharwar Date: Thu, 10 Sep 2026 11:16:42 +0530 Subject: [PATCH 1/3] fix(visualizer): prevent duplicate workflow entry activities --- tldrgraph/visualizer/assets/app.js | 22 +- uv.lock | 377 ++++++++++++++++++++++++++++- 2 files changed, 384 insertions(+), 15 deletions(-) diff --git a/tldrgraph/visualizer/assets/app.js b/tldrgraph/visualizer/assets/app.js index 8f9db0d..a238fc2 100644 --- a/tldrgraph/visualizer/assets/app.js +++ b/tldrgraph/visualizer/assets/app.js @@ -3123,7 +3123,10 @@ function buildWorkflowLayout(w) { const groups = groupByStep(visible, flows); const spineIds = []; - const stepEntry = []; + // The spine card is the step's entry activity. Keep this mapping so flows + // that originally start at that activity can enter its internal detail + // directly, rather than rendering the same activity a second time below it. + const spineForHead = new Map(); // How much room each step needs: its widest level of branch shapes. const slotWidth = groups.map(group => { @@ -3197,9 +3200,6 @@ function buildWorkflowLayout(w) { // step never stretches the whole diagram sideways. const rows = []; const levelKeys = Array.from(group.levels.keys()).sort((a, b) => a - b); - if (!bare) { - rows.push([head]); // the head is now a branch shape - } levelKeys.forEach(level => { const list = group.levels.get(level); for (let i = 0; i < list.length; i += BRANCH_PER_LEVEL) { @@ -3225,9 +3225,7 @@ function buildWorkflowLayout(w) { }); }); - if (!bare) { - stepEntry.push({ spine: lineNode.id, head: head.id }); - } + if (!bare) spineForHead.set(head.id, lineNode.id); cursorX += slot + NODE_GAP; }); @@ -3243,16 +3241,14 @@ function buildWorkflowLayout(w) { for (let i = 0; i < spineIds.length - 1; i++) { rebuilt.push({ source: spineIds[i], target: spineIds[i + 1], label: '', kind: 'sequence' }); } - stepEntry.forEach(pair => { - rebuilt.push({ source: pair.spine, target: pair.head, label: '', kind: 'enter' }); - }); flows.forEach(f => { - const a = placedById.get(f.source); + const source = spineForHead.get(f.source) || f.source; + const a = placedById.get(source); const b = placedById.get(f.target); if (!a || !b) return; - if (f.kind === 'loop_back') { rebuilt.push(f); return; } + if (f.kind === 'loop_back') { rebuilt.push({ ...f, source: source }); return; } if (stepOf.get(f.source) !== stepOf.get(f.target)) return; // the line covers this - rebuilt.push(f); + rebuilt.push({ ...f, source: source }); }); flowEdges = rebuilt.map(f => { diff --git a/uv.lock b/uv.lock index 0df668a..b359687 100644 --- a/uv.lock +++ b/uv.lock @@ -23,6 +23,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/56/4744bcd0c82184e80c52b0ac4076c261a8ffa1f1b343ff2f6e89ce0e1cef/backrefs-8.0.tar.gz", hash = "sha256:b556cd7d36c3a3a2f256b89590b176b8eddfb73bcfaee3a3ddd84ea66d21ce50", size = 7013081, upload-time = "2026-07-26T19:54:24.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/fd/9bf53b6a6f6f519ffaac765df2f2a25e5c2fc6d32cfd2b2747099e72c911/backrefs-8.0-py310-none-any.whl", hash = "sha256:4a627b817fd2dce43b79ab48da63613340509381cd8ce0897078a0bce79a2ab8", size = 380377, upload-time = "2026-07-26T19:54:17.457Z" }, + { url = "https://files.pythonhosted.org/packages/e1/29/4bd7ae72a2634da00379c2b3bcc5439e7c94620235c6afea8af15229a973/backrefs-8.0-py311-none-any.whl", hash = "sha256:f0c35cf0102ba6b6070c12a492be3c1c1d3f5839529784b9a9565d6d04569a01", size = 392169, upload-time = "2026-07-26T19:54:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/29/13/232505664e8e2a0c7a2eb0c505cfade9d715538f89a5d62bc4c272968f62/backrefs-8.0-py312-none-any.whl", hash = "sha256:87f0fae8c5f207fe9f4b2887efc71d42f4900ac78faa1af08d675ef303692dc5", size = 398084, upload-time = "2026-07-26T19:54:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/8a/69/47a3dc20abc4fa5486655fde681bd55e63211b46c886d8c02223d6468431/backrefs-8.0-py313-none-any.whl", hash = "sha256:601ce68ca12385dbda06ce264406b4c4210cf5b79fd0fd627592365c92f29a88", size = 400040, upload-time = "2026-07-26T19:54:21.194Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/e5f9b68a5b0e939a2fb933a66c20180d0c9241bf8927f7a47fa48c1675e9/backrefs-8.0-py314-none-any.whl", hash = "sha256:9ec96efa080938be92323e8e730e57718c9c88eb15ad70bbef4e1766df591408", size = 411903, upload-time = "2026-07-26T19:54:23.221Z" }, +] + [[package]] name = "certifi" version = "2026.7.22" @@ -225,6 +247,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, ] +[[package]] +name = "csscompressor" +version = "0.9.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/2a/8c3ac3d8bc94e6de8d7ae270bb5bc437b210bb9d6d9e46630c98f4abd20c/csscompressor-0.9.5.tar.gz", hash = "sha256:afa22badbcf3120a4f392e4d22f9fff485c044a1feda4a950ecc5eba9dd31a05", size = 237808, upload-time = "2017-11-26T21:13:08.238Z" } + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -287,6 +315,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + [[package]] name = "graphifyy" version = "0.9.53" @@ -364,6 +404,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" }, ] +[[package]] +name = "htmlmin2" +version = "0.1.13" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/31/a76f4bfa885f93b8167cb4c85cf32b54d1f64384d0b897d45bc6d19b7b45/htmlmin2-0.1.13-py3-none-any.whl", hash = "sha256:75609f2a42e64f7ce57dbff28a39890363bde9e7e5885db633317efbdf8c79a2", size = 34486, upload-time = "2023-03-14T21:28:30.388Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -433,6 +481,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jsmin" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/73/e01e4c5e11ad0494f4407a3f623ad4d87714909f50b17a06ed121034ff6e/jsmin-3.0.1.tar.gz", hash = "sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc", size = 13925, upload-time = "2022-01-16T20:35:59.13Z" } + [[package]] name = "loguru" version = "0.7.3" @@ -446,6 +512,193 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, ] +[[package]] +name = "markdown" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/6f/da4c6aea59b3001f2e8c0ec7497475aadaf3b021c10cab5b2858f0f32b26/markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f", size = 372596, upload-time = "2026-07-30T19:05:29.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/cd/c05d3a530ba7934f144fb45f7203cd236adc25c7bdcc34673d202f4b0278/mkdocs_material-9.7.7.tar.gz", hash = "sha256:c0649c065b1b0512d60aad8c10f947f8e455284475239b364b610f2deb4d0855", size = 4097923, upload-time = "2026-07-17T16:21:33.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/21/17c1bc9e6f47c972ad66fb2ac2568f99f90f1207eeb6fc3b34d094dba7b5/mkdocs_material-9.7.7-py3-none-any.whl", hash = "sha256:8ea9bb1737a5b524a5f9dcf2e1b4ebda8274ae3008aa7845720a97083bef708f", size = 9305438, upload-time = "2026-07-17T16:21:30.017Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocs-minify-plugin" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "csscompressor" }, + { name = "htmlmin2" }, + { name = "jsmin" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/67/fe4b77e7a8ae7628392e28b14122588beaf6078b53eb91c7ed000fd158ac/mkdocs-minify-plugin-0.8.0.tar.gz", hash = "sha256:bc11b78b8120d79e817308e2b11539d790d21445eb63df831e393f76e52e753d", size = 8366, upload-time = "2024-01-29T16:11:32.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/cd/2e8d0d92421916e2ea4ff97f10a544a9bd5588eb747556701c983581df13/mkdocs_minify_plugin-0.8.0-py3-none-any.whl", hash = "sha256:5fba1a3f7bd9a2142c9954a6559a57e946587b21f133165ece30ea145c66aee6", size = 6723, upload-time = "2024-01-29T16:11:31.851Z" }, +] + [[package]] name = "mmh3" version = "5.3.0" @@ -951,6 +1204,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + [[package]] name = "pillow" version = "12.3.0" @@ -1045,6 +1316,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, ] +[[package]] +name = "platformdirs" +version = "4.11.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/18/f3bb8ef0d3b930692343da8aa4d3cbcd6749477c053959395ac81965a6e9/platformdirs-4.11.8.tar.gz", hash = "sha256:f23abafea7dd4276d1f29104b83598d7dcc567cafd07c9c951e66665645437fc", size = 37182, upload-time = "2026-09-08T22:20:42.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/e1/5b7b8bbb55084d1425bcb9bc823ff519e1b2be05f6ebb0089e2eacc38413/platformdirs-4.11.8-py3-none-any.whl", hash = "sha256:52f2f181bbfde907966932cc8312d967d02976422d66d537ea16092b8e291081", size = 24027, upload-time = "2026-09-08T22:20:41.537Z" }, +] + [[package]] name = "protobuf" version = "7.36.1" @@ -1122,6 +1402,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/78/3bf351dbcc7f51eb03a506c0bcf8aead8b1401cf26aaa1328968471531aa/py_rust_stemmers-0.1.8-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc185b599e646a0e39d11df3f5e6d15edefb110496601556385d33b55fed5de", size = 320180, upload-time = "2026-05-22T11:00:23.387Z" }, ] +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "11.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/17/2db4b414de89659144488e0d9c6c0bf0c8395841dc12d81d0532cc6ef310/pymdown_extensions-11.0.2.tar.gz", hash = "sha256:9506fcbe66fa355a775b768084334238dd6805020ac4b92bea0c0dda6f8f223d", size = 855419, upload-time = "2026-08-22T19:28:47.236Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/43/9f45ec4d14e596efc32c925a78104934790438b0c0628b70d741016734ad/pymdown_extensions-11.0.2-py3-none-any.whl", hash = "sha256:259910762019732caa1dfd76f3faa62c59f191d46573e80bcb1d13c0f675bbe5", size = 269929, upload-time = "2026-08-22T19:28:45.389Z" }, +] + [[package]] name = "pyreadline3" version = "3.5.6" @@ -1131,6 +1433,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1195,6 +1509,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "rapidfuzz" version = "3.14.5" @@ -1419,6 +1745,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sympy" version = "1.14.0" @@ -1442,7 +1777,7 @@ wheels = [ [[package]] name = "tldrgraph" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "click" }, @@ -1461,6 +1796,10 @@ dependencies = [ ] [package.optional-dependencies] +docs = [ + { name = "mkdocs-material" }, + { name = "mkdocs-minify-plugin" }, +] embeddings = [ { name = "fastembed" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1474,6 +1813,8 @@ requires-dist = [ { name = "fastembed", specifier = ">=0.3" }, { name = "fastembed", marker = "extra == 'embeddings'", specifier = ">=0.3" }, { name = "graphifyy", specifier = ">=0.9.0" }, + { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.5.0" }, + { name = "mkdocs-minify-plugin", marker = "extra == 'docs'", specifier = ">=0.8.0" }, { name = "networkx", specifier = ">=3.0" }, { name = "numpy", specifier = ">=1.24" }, { name = "numpy", marker = "extra == 'embeddings'", specifier = ">=1.24" }, @@ -1483,7 +1824,7 @@ requires-dist = [ { name = "tree-sitter-javascript", specifier = ">=0.21" }, { name = "tree-sitter-typescript", specifier = ">=0.21" }, ] -provides-extras = ["embeddings"] +provides-extras = ["embeddings", "docs"] [[package]] name = "tokenizers" @@ -1971,6 +2312,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + [[package]] name = "win32-setctime" version = "1.2.0" From 42704a2c253c44b72f32f2f433f38342114036ca Mon Sep 17 00:00:00 2001 From: Ashwani Kharwar Date: Thu, 10 Sep 2026 15:01:29 +0530 Subject: [PATCH 2/3] feat: streamline agent init and improve workflow labels - Auto-approve full enrichment runs in coding-agent sessions - Require LLM route-link inference unless explicitly disabled - Improve workflow action labels and call resolution - Add regression coverage for init, BPMN, and visualizer behavior --- .agents/skills/tldrgraph-init/SKILL.md | 33 ++++---- .claude/commands/tldrgraph-init.md | 33 ++++---- AGENTS.md | 16 ++-- AGENT_CONTRACT.md | 11 +-- README.md | 11 +-- tests/test_auto_agent.py | 56 +++++++++++++ tests/test_bpmn.py | 105 +++++++++++++++++++++++++ tldrgraph/agent_commands.py | 29 +++---- tldrgraph/cli_llm_links.py | 6 +- tldrgraph/cli_pipeline.py | 11 ++- tldrgraph/installer_contract.py | 9 ++- tldrgraph/visualizer/action_labels.py | 81 +++++++++++++++++++ tldrgraph/visualizer/assets/app.js | 17 ++-- tldrgraph/visualizer/bpmn_data.py | 31 ++------ tldrgraph/visualizer/flows_discover.py | 30 ++----- 15 files changed, 354 insertions(+), 125 deletions(-) create mode 100644 tldrgraph/visualizer/action_labels.py diff --git a/.agents/skills/tldrgraph-init/SKILL.md b/.agents/skills/tldrgraph-init/SKILL.md index 86dccf1..e7e58cf 100644 --- a/.agents/skills/tldrgraph-init/SKILL.md +++ b/.agents/skills/tldrgraph-init/SKILL.md @@ -14,21 +14,21 @@ One command handles layer design, extraction, enrichment, LLM route links, and e tldrgraph init ``` -By default TLDRGraph detects `claude`, `cursor-agent`, or `gemini`, asks once before enrichment token spend, processes every +By default TLDRGraph detects `claude`, `cursor-agent`, or `gemini`, processes every eligible node in batches of 200, and downloads/builds the local embedding model. -Use `--yes` for approval, `--batch N` for batch size, `--embeddings off|auto|on`, -`--no-llm-links` to skip LLM frontend/backend route inference, or `--no-agent-cli` -for manual file handoff. +In a detected coding-agent session, plain `tldrgraph init` auto-approves the full +enrichment campaign; normal terminal users and non-agent automation still get the +confirmation gate. -After the user approves the full run, use exactly `tldrgraph init --yes`. The -approval is saved for the current candidate set, so later `tldrgraph init` calls -must continue without asking again. `--batch 200` means all nodes in 200-node -batches; `--limit 200` means stop after only 200 nodes. Never add `--limit` or -`--embeddings off` unless the user explicitly requests a partial or no-embedding run. +Use exactly `tldrgraph init` for this workflow. Add `--yes` only if a non-agent +`needs_confirmation` response explicitly asks for approval. `--batch 200` means +all nodes in 200-node batches; `--limit 200` means stop after only 200 nodes. +Never add `--limit` or `--embeddings off` unless the user explicitly requests a +partial or no-embedding run. ## Completion contract for agents -Once the user approves a full enrichment campaign, you are not done until one of +Once a full enrichment campaign is approved or auto-approved, you are not done until one of these terminal states occurs: - `tldrgraph init` reports `status: done`. @@ -40,6 +40,7 @@ these terminal states occurs: These are **not** terminal states: - `status: needs_enrichment` +- `status: needs_llm_links` - `NEXT ACTION` - "nodes remaining" - "batches remaining" @@ -108,9 +109,9 @@ Design one from this repository. ## `status: needs_confirmation` -The output shows how many nodes need enrichment and how many agent round-trips -that implies. **Ask the user whether to proceed, and show them that estimate.** -Do not decide for them. +Detected coding-agent sessions should not reach this state for a full run. If a +non-agent run does, the output shows how many nodes need enrichment and how many +agent round-trips that implies; ask the user whether to proceed. - They agree: `tldrgraph init --yes` saves approval for the full campaign - Smaller first pass: `tldrgraph init --yes --limit 100` @@ -140,7 +141,8 @@ count as sentences. 4. Run `tldrgraph init` again. Approval is saved; process any next `needs_enrichment` batch immediately without asking the user again until - `status: done` or `needs_llm_links`. + init either reports `status: done` or advances to the required `needs_llm_links` + phase. Inside an existing Codex/Claude/Cursor session, nested-agent protection may stop the CLI from launching a second agent. In that case **you are the enrichment @@ -160,7 +162,8 @@ empty list is a correct answer, a wrong `calls` entry becomes a real wrong edge. 3. Write `.tldrgraph/llm_links_response.yaml` as a YAML list of `{source, target, confidence, frontend_evidence, backend_evidence, explanation}`. Only include source-backed links with file and line evidence. -4. Run `tldrgraph init` again, or use `--no-llm-links` to skip this optional stage. +4. Run `tldrgraph init` again. This is a required continuation state for a + complete init run unless the user explicitly requested `--no-llm-links`. ## Once it says DONE diff --git a/.claude/commands/tldrgraph-init.md b/.claude/commands/tldrgraph-init.md index 86dccf1..e7e58cf 100644 --- a/.claude/commands/tldrgraph-init.md +++ b/.claude/commands/tldrgraph-init.md @@ -14,21 +14,21 @@ One command handles layer design, extraction, enrichment, LLM route links, and e tldrgraph init ``` -By default TLDRGraph detects `claude`, `cursor-agent`, or `gemini`, asks once before enrichment token spend, processes every +By default TLDRGraph detects `claude`, `cursor-agent`, or `gemini`, processes every eligible node in batches of 200, and downloads/builds the local embedding model. -Use `--yes` for approval, `--batch N` for batch size, `--embeddings off|auto|on`, -`--no-llm-links` to skip LLM frontend/backend route inference, or `--no-agent-cli` -for manual file handoff. +In a detected coding-agent session, plain `tldrgraph init` auto-approves the full +enrichment campaign; normal terminal users and non-agent automation still get the +confirmation gate. -After the user approves the full run, use exactly `tldrgraph init --yes`. The -approval is saved for the current candidate set, so later `tldrgraph init` calls -must continue without asking again. `--batch 200` means all nodes in 200-node -batches; `--limit 200` means stop after only 200 nodes. Never add `--limit` or -`--embeddings off` unless the user explicitly requests a partial or no-embedding run. +Use exactly `tldrgraph init` for this workflow. Add `--yes` only if a non-agent +`needs_confirmation` response explicitly asks for approval. `--batch 200` means +all nodes in 200-node batches; `--limit 200` means stop after only 200 nodes. +Never add `--limit` or `--embeddings off` unless the user explicitly requests a +partial or no-embedding run. ## Completion contract for agents -Once the user approves a full enrichment campaign, you are not done until one of +Once a full enrichment campaign is approved or auto-approved, you are not done until one of these terminal states occurs: - `tldrgraph init` reports `status: done`. @@ -40,6 +40,7 @@ these terminal states occurs: These are **not** terminal states: - `status: needs_enrichment` +- `status: needs_llm_links` - `NEXT ACTION` - "nodes remaining" - "batches remaining" @@ -108,9 +109,9 @@ Design one from this repository. ## `status: needs_confirmation` -The output shows how many nodes need enrichment and how many agent round-trips -that implies. **Ask the user whether to proceed, and show them that estimate.** -Do not decide for them. +Detected coding-agent sessions should not reach this state for a full run. If a +non-agent run does, the output shows how many nodes need enrichment and how many +agent round-trips that implies; ask the user whether to proceed. - They agree: `tldrgraph init --yes` saves approval for the full campaign - Smaller first pass: `tldrgraph init --yes --limit 100` @@ -140,7 +141,8 @@ count as sentences. 4. Run `tldrgraph init` again. Approval is saved; process any next `needs_enrichment` batch immediately without asking the user again until - `status: done` or `needs_llm_links`. + init either reports `status: done` or advances to the required `needs_llm_links` + phase. Inside an existing Codex/Claude/Cursor session, nested-agent protection may stop the CLI from launching a second agent. In that case **you are the enrichment @@ -160,7 +162,8 @@ empty list is a correct answer, a wrong `calls` entry becomes a real wrong edge. 3. Write `.tldrgraph/llm_links_response.yaml` as a YAML list of `{source, target, confidence, frontend_evidence, backend_evidence, explanation}`. Only include source-backed links with file and line evidence. -4. Run `tldrgraph init` again, or use `--no-llm-links` to skip this optional stage. +4. Run `tldrgraph init` again. This is a required continuation state for a + complete init run unless the user explicitly requested `--no-llm-links`. ## Once it says DONE diff --git a/AGENTS.md b/AGENTS.md index 16a30e2..5fdcbf7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,16 +24,16 @@ tldrgraph dead-code # review candidates, never a dele Those are read-only and never trigger enrichment. -**To build or refresh the graph**, run `tldrgraph init`. It automatically handles -layer design, extraction, source-aware enrichment in 200-node batches, LLM -frontend/backend route-link inference, and dense embeddings when a supported -agent CLI is available. If it prints a `NEXT ACTION` fallback, follow that -handoff without guessing from symbol names. +**To build or refresh the graph**, run `tldrgraph init`. In a detected coding-agent +session, plain `tldrgraph init` auto-approves the full enrichment campaign; normal +terminal users and non-agent automation still get confirmation. It handles layer +design, extraction, source-aware enrichment in 200-node batches, LLM route links, +and dense embeddings. If it prints a `NEXT ACTION` fallback, follow that handoff. ### ✅ TLDRGraph Init Completion Contract -After the user approves a full TLDRGraph enrichment run, the agent MUST keep -working until `tldrgraph init` reports `status: done` or a genuine blocking error -requires user action. +After a full TLDRGraph enrichment run is approved or auto-approved, the agent MUST +keep working until `tldrgraph init` reports `status: done` or a genuine blocking +error requires user action. - `needs_enrichment`, "nodes remaining", "batches remaining", and "NEXT ACTION" are continuation states, not completion states. diff --git a/AGENT_CONTRACT.md b/AGENT_CONTRACT.md index b2655be..2c1aac6 100644 --- a/AGENT_CONTRACT.md +++ b/AGENT_CONTRACT.md @@ -24,14 +24,15 @@ One command handles layers, extraction, enrichment, and embeddings: tldrgraph init ``` -It asks once before enrichment token spend. Full approval is persisted for the current -candidate set until enrichment finishes, so continuation runs must not ask again. By -default it uses 200-node batches and builds dense embeddings. +In a detected coding-agent session, plain `tldrgraph init` auto-approves the full +enrichment campaign. Normal terminal users and non-agent automation still get the +confirmation gate. Full approval is persisted for the current candidate set until +enrichment finishes, so continuation runs must not ask again. | status | what it wants | | --- | --- | | `needs_layers` | Read the code and design this repository's architecture. **TLDRGraph ships no layer templates**; nothing will be applied for you. The request carries sketches of how other kinds of codebase divide — for shape only, never to copy. | -| `needs_confirmation` | Show the estimate and ask once. Approval via `tldrgraph init --yes` persists until the current campaign is done. | +| `needs_confirmation` | Non-agent runs only: show the estimate and ask once. Approval via `tldrgraph init --yes` persists until the current campaign is done. | | `needs_enrichment` | Open, read, and describe this batch, then continue immediately without asking again. | | `needs_embeddings` | Enrichment finished but the required dense model/index could not be built. Fix model access and rerun init. | | `done` | Nothing left. Use `query` / `trace` / `layers`. | @@ -48,7 +49,7 @@ dropped, and will be reported back to you — but the work is wasted. ## The loop ```bash -tldrgraph init --yes # 1. approve every current candidate; writes a 200-node request +tldrgraph init # 1. approve the full campaign in agent sessions # 2. read every requested source and write enrichment_response.yaml tldrgraph init # 3. applies it and emits the next batch; approval is remembered # 4. repeat steps 2-3 without asking until status: done diff --git a/README.md b/README.md index f0f3d84..3482061 100644 --- a/README.md +++ b/README.md @@ -81,10 +81,11 @@ repository, runs `init`, and handles every required follow-up. In Claude Code or Cursor, run `/tldrgraph-init`. In Codex, open `/skills` and select `tldrgraph-init`, or invoke `$tldrgraph-init`. -The agent designs the repository-specific layers, extracts the graph, asks once -before enrichment token spend, enriches every eligible node in 200-node batches, -and downloads/builds local dense embeddings. That approval is remembered for the -current candidate set until enrichment is complete. +The agent designs the repository-specific layers, extracts the graph, enriches +every eligible node in 200-node batches, and downloads/builds local dense +embeddings. In a detected coding-agent session, plain `tldrgraph init` +auto-approves the full enrichment campaign; normal terminal users and non-agent +automation still get the confirmation gate. `--batch 200` controls chunk size while still processing everything. `--limit 200` intentionally stops after 200 total nodes. Embeddings remain enabled unless @@ -99,7 +100,7 @@ It can report four resumable states: | status | what it needs | | --- | --- | | `needs_layers` | Read the code and design the architecture. No template will be applied for you. | -| `needs_confirmation` | Shows how many nodes need enrichment and how many agent rounds that is. **Your agent asks you before spending tokens.** | +| `needs_confirmation` | Non-agent runs only: shows how many nodes need enrichment and how many agent rounds that is before approval. | | `needs_enrichment` | A batch of nodes to open, read, and describe. | | `needs_embeddings` | Enrichment is complete, but the required dense model/index could not be built. | diff --git a/tests/test_auto_agent.py b/tests/test_auto_agent.py index 92289d4..1f01dec 100644 --- a/tests/test_auto_agent.py +++ b/tests/test_auto_agent.py @@ -371,6 +371,30 @@ def test_init_asks_before_spending_tokens_and_shows_the_estimate(cli_repo): assert "tldrgraph init --yes" in res.output +def test_coding_agent_init_auto_approves_full_campaign(monkeypatch, cli_repo): + _answer_layers(cli_repo) + monkeypatch.setenv("AI_AGENT", "1") + + res = CliRunner().invoke(cli, ["init", str(cli_repo)]) + + assert res.exit_code == 0, res.output + assert "Detected coding-agent session ($AI_AGENT)" in res.output + assert "status: needs_confirmation" not in res.output + assert "status: needs_enrichment" in res.output + assert (cli_repo / ".tldrgraph" / APPROVAL_FILENAME).is_file() + + +def test_coding_agent_init_with_limit_is_not_full_auto_approval(monkeypatch, cli_repo): + _answer_layers(cli_repo) + monkeypatch.setenv("AI_AGENT", "1") + + res = CliRunner().invoke(cli, ["init", str(cli_repo), "--limit", "1"]) + + assert res.exit_code == 0, res.output + assert "status: needs_confirmation" in res.output + assert not (cli_repo / ".tldrgraph" / APPROVAL_FILENAME).exists() + + def test_the_estimate_is_machine_readable(cli_repo): _answer_layers(cli_repo) res = CliRunner().invoke(cli, ["init", str(cli_repo), "--json"]) @@ -621,6 +645,38 @@ def _answer(prompt): assert edges[0]["frontend_file"].endswith("page.tsx") +def test_llm_route_failure_requires_manual_handoff(monkeypatch, cli_repo, agent_allowed): + (cli_repo / "frontend/src/orders").mkdir(parents=True) + (cli_repo / "backend/src").mkdir(parents=True) + (cli_repo / "frontend/src/orders/page.tsx").write_text( + "export function OrdersPage() { return getOrders() }\n", encoding="utf-8" + ) + (cli_repo / "backend/src/orders.controller.ts").write_text( + "@Controller('orders')\nexport class OrdersController {\n" + " @Get()\n findAll() { return [] }\n}\n", + encoding="utf-8", + ) + + def _answer(agent, prompt, cwd, timeout=None, model=None): + if _is_llm_links_prompt(prompt): + raise agent_runner.AgentError("route agent unavailable") + return _fake_answer(prompt) + + monkeypatch.setattr(agent_runner, "run_agent", _answer) + monkeypatch.setattr(agent_runner, "find_agent_cli", lambda **kw: fake_agent()) + monkeypatch.setattr( + agent_runner, "agent_status", + lambda: {"agent": fake_agent(), "reason": "ready", "detail": "Fake fake"}, + ) + res = CliRunner().invoke(cli, ["init", str(cli_repo), "--yes", "--agent-cli"]) + + assert res.exit_code == 0, res.output + assert "status: needs_llm_links" in res.output + assert "Required LLM frontend-backend link inference" in res.output + assert "optional" not in res.output.lower() + assert (cli_repo / ".tldrgraph" / "llm_links_request.yaml").is_file() + + def test_no_llm_links_flag_skips_route_inference(monkeypatch, cli_repo, agent_allowed): calls = [] diff --git a/tests/test_bpmn.py b/tests/test_bpmn.py index 661e81e..138702c 100644 --- a/tests/test_bpmn.py +++ b/tests/test_bpmn.py @@ -9,7 +9,9 @@ from tldrgraph.visualizer.bpmn_data import ( LANE_EXTERNAL, LANE_SYSTEM, + _build_label_index, _question_from, + _resolve_call, build_workflow_process, ) from tldrgraph.visualizer.bpmn_phrasing import ELEMENT_PHRASES, phrase_for_element @@ -87,6 +89,22 @@ def run(data, path): assert externals == {"File system"} +def test_browser_fetch_does_not_resolve_to_unrelated_project_method(): + nodes_by_id = { + "exchange": {"label": "exchangeImpersonationCode()", "file": "frontend/src/services/auth.ts"}, + "github_fetch": {"label": ".fetch()", "file": "backend/src/services/githubSync.ts"}, + } + + resolved = _resolve_call( + ["fetch"], + "frontend/src/services/auth.ts", + nodes_by_id, + _build_label_index(nodes_by_id), + ) + + assert resolved is None + + def _workflow(tmp_path): return { "id": "flow_test", @@ -285,6 +303,93 @@ def test_tests_and_vendored_code_are_never_offered_as_a_journey(): assert rank_entry_points(graph, nodes_by_id, 10) == [] +def test_workflow_process_prefers_intent_action_over_code_label(tmp_path): + graph = nx.DiGraph() + nodes_by_id = { + "endpoint": { + "label": "POST /containers/create", + "file": "backend/src/routes/containers.ts", + "layer_id": "api", + "layer": "API", + "intent": "Create a project container for the user. It validates the request and returns the new container.", + }, + "schema": { + "label": "Project", + "file": "schema.prisma", + "layer_id": "data", + "layer": "Data", + "intent": "Store the project record in the database. It keeps the workspace metadata available for later steps.", + }, + } + graph.add_nodes_from((node_id, data) for node_id, data in nodes_by_id.items()) + + workflow = { + "id": "flow_project_prompt", + "steps": [ + {"node_id": "endpoint", "symbol": "POST /containers/create", "file": "missing.ts", "code_start": 1, + "intent": nodes_by_id["endpoint"]["intent"]}, + {"node_id": "schema", "symbol": "Project", "file": "schema.prisma", "code_start": 1, + "intent": nodes_by_id["schema"]["intent"]}, + ], + } + + process = build_workflow_process(str(tmp_path), workflow, graph, nodes_by_id) + labels = [e["label"] for e in process["elements"] if e.get("node_id")] + titles = [e["step_title"] for e in process["elements"] if e.get("node_id")] + + assert labels == [ + "Create a project container for the user", + "Store the project record in the database", + ] + assert titles == labels + assert "POST /containers/create" not in labels + assert "Project" not in labels + + +def test_workflow_process_rejects_generic_symbol_intents(tmp_path): + graph = nx.DiGraph() + nodes_by_id = { + "endpoint": { + "label": "POST /containers/create", + "file": "backend/src/routes/containers.ts", + "layer_id": "api", + "layer": "API", + "intent": ( + "The symbol POST /containers/create is defined in backend/src/routes/" + "containers.ts as an HTTP-facing backend module." + ), + }, + "handler": { + "label": "POST /containers/create handler", + "file": "backend/src/routes/containers.ts", + "layer_id": "api", + "layer": "API", + "intent": "The symbol POST /containers/create handler is defined in backend/src/routes/containers.ts.", + }, + "schema": { + "label": "Project", + "file": "schema.prisma", + "layer_id": "persistence", + "layer": "Persistence & Domain Data", + "intent": "The symbol Project is defined in schema.prisma as a data model.", + }, + } + graph.add_nodes_from((node_id, data) for node_id, data in nodes_by_id.items()) + workflow = { + "id": "flow_project_prompt", + "steps": [ + {"node_id": node_id, "symbol": node["label"], "file": node["file"], "intent": node["intent"]} + for node_id, node in nodes_by_id.items() + ], + } + + process = build_workflow_process(str(tmp_path), workflow, graph, nodes_by_id) + labels = [e["label"] for e in process["elements"] if e.get("node_id")] + + assert labels == ["Create container", "Handle create container request", "Store project data"] + assert all(not label.startswith("The symbol") for label in labels) + + def test_enrichment_round_trip_stores_phrases_against_their_code(tmp_path): from tldrgraph.bpmn_enrichment import apply_response, collect_candidates, load_store, write_request diff --git a/tldrgraph/agent_commands.py b/tldrgraph/agent_commands.py index e6f39b5..c111b2b 100644 --- a/tldrgraph/agent_commands.py +++ b/tldrgraph/agent_commands.py @@ -149,16 +149,16 @@ class AgentTarget: tldrgraph init ``` -By default TLDRGraph detects `claude`, `cursor-agent`, or `gemini`, asks once -before enrichment token spend, processes 200-node batches, and builds embeddings. -Use `--yes` for approval, `--batch N` for batch size, `--embeddings off|auto|on`, -`--no-llm-links` to skip LLM frontend/backend route inference, or `--no-agent-cli` -for manual file handoff. +By default TLDRGraph detects `claude`, `cursor-agent`, or `gemini`, processes +200-node batches, and builds embeddings. In a detected coding-agent session, +plain `tldrgraph init` auto-approves the full enrichment campaign; normal +terminal users and non-agent automation still get the confirmation gate. -After approval, use exactly `tldrgraph init --yes`; later continuation runs must -continue without asking the user again. `--batch 200` means all nodes in chunks; -`--limit 200` means stop after only 200 nodes. Never add `--limit` or -`--embeddings off` unless the user explicitly requests it. +Use exactly `tldrgraph init` for this workflow. Add `--yes` only if a non-agent +`needs_confirmation` response explicitly asks for approval. `--batch 200` means +all nodes in chunks; `--limit 200` means stop after only 200 nodes. +Never add `--limit`, `--no-llm-links`, or `--embeddings off` unless the user +explicitly requests it. If no supported agent is available or dense embeddings cannot be built, `init` preserves the graph and prints a resumable status. It never guesses source intent, @@ -201,9 +201,9 @@ class AgentTarget: ## `status: needs_confirmation` -The output shows how many nodes need enrichment and how many agent round-trips -that implies. **Ask the user whether to proceed, and show them that estimate.** -Do not decide for them. +Detected coding-agent sessions should not reach this state for a full run. If a +non-agent run does, the output shows how many nodes need enrichment and how many +agent round-trips that implies; ask the user whether to proceed. - They agree: `tldrgraph init --yes` saves approval for the full campaign - Smaller first pass: `tldrgraph init --yes --limit 100` @@ -232,7 +232,7 @@ class AgentTarget: count as sentences. 4. Run `tldrgraph init` again. Approval is saved; process any next - `needs_enrichment` batch immediately until `status: done` or `needs_llm_links`. + `needs_enrichment` batch immediately without asking the user again. Inside an existing Codex/Claude/Cursor session, nested-agent protection may stop the CLI from launching a second agent. In that case **you are the enrichment @@ -251,7 +251,8 @@ class AgentTarget: files, then write `.tldrgraph/llm_links_response.yaml` as a YAML list of `{source, target, confidence, frontend_evidence, backend_evidence, explanation}`. Only include source-backed links with file and line evidence. Run `tldrgraph init` -again, or use `--no-llm-links` to skip this optional stage. +again. This is a required continuation state for a complete init run unless the +user explicitly requested `--no-llm-links`. ## Once it says DONE diff --git a/tldrgraph/cli_llm_links.py b/tldrgraph/cli_llm_links.py index 6c8d717..4b5481f 100644 --- a/tldrgraph/cli_llm_links.py +++ b/tldrgraph/cli_llm_links.py @@ -100,7 +100,7 @@ def run_llm_link_step( except agent_runner.AgentError as err: if not as_json: click.echo(f" ⚠️ LLM route link inference failed: {err}") - return None + click.echo(" Falling back to the required file handoff.") req_path = write_payload(state_path(path, REQUEST_FILENAME), { "schema": "codechakra/llm-route-links-request@1", @@ -114,13 +114,11 @@ def run_llm_link_step( "payload": payload, }) emit_status(STATUS_NEEDS_LLM_LINKS, "llm_links", [ - "The graph is built and enriched. LLM frontend-backend link inference needs the active agent:", + "The graph is built and enriched. Required LLM frontend-backend link inference needs the active agent:", "", f" 1. Read {os.path.relpath(req_path, root)}", " 2. Open the referenced frontend and backend source files.", f" 3. Write .tldrgraph/{RESPONSE_FILENAME} with strict evidence for every link.", " 4. Run: tldrgraph init", - "", - "Use --no-llm-links to skip this optional inference stage.", ], progress={"candidate_hash": candidate_hash}, as_json=as_json) return STATUS_NEEDS_LLM_LINKS diff --git a/tldrgraph/cli_pipeline.py b/tldrgraph/cli_pipeline.py index de8999b..a8c7194 100644 --- a/tldrgraph/cli_pipeline.py +++ b/tldrgraph/cli_pipeline.py @@ -151,7 +151,8 @@ def _run_agent_cli_enrichment( status = STATUS_NEEDS_ENRICHMENT if rem else ( STATUS_NEEDS_EMBEDDINGS if embedding_error else STATUS_DONE ) - retry = ["Run `tldrgraph init --yes` to continue."] if rem or embedding_error else [] + resume = "tldrgraph init" if progress.get("approval_persisted") else "tldrgraph init --yes" + retry = [f"Run `{resume}` to continue."] if rem or embedding_error else [] emit_status(status, "embeddings" if embedding_error else "enrichment", [ f"Enriched {totals['applied']} node(s) in {totals['batches']} batch(es); {totals['bridges']} bridge edge(s).", f"⚠️ {totals['intent_length_violations']} intent(s) were outside the recommended 2-3 sentences." if totals["intent_length_violations"] else "All applied intents met the recommended 2-3 sentence length.", @@ -196,7 +197,7 @@ def _emit_enrichment_done(loader: GraphLoader, total: int, enriched: int, exclud if embedding_error: lines.extend([ f"Dense embeddings could not be completed: {embedding_error}", - "Run `tldrgraph init --yes` again after fixing model access.", + "Run `tldrgraph init` again after fixing model access.", ]) else: lines.extend([ @@ -254,12 +255,16 @@ def _handle_enrichment_step( "approval_persisted": enrichment_approval_is_active(path, candidates), } - authorized = assume_yes or enrichment_approval_is_active(path, candidates) + agent_marker = agent_runner.nesting_marker() + auto_agent_approved = bool(agent_marker) and not max_nodes + authorized = assume_yes or enrichment_approval_is_active(path, candidates) or auto_agent_approved if not authorized: conf_status = _check_confirmation(candidates, total, enriched, excluded, rounds, batch_size, progress, as_json) if conf_status: return conf_status authorized = True + elif auto_agent_approved and not assume_yes and not as_json: + click.echo(f"🤖 Detected coding-agent session (${agent_marker}); proceeding with full enrichment.") if authorized and not max_nodes: remember_full_enrichment_approval(path, candidates) diff --git a/tldrgraph/installer_contract.py b/tldrgraph/installer_contract.py index cff1ec9..dcf52fb 100644 --- a/tldrgraph/installer_contract.py +++ b/tldrgraph/installer_contract.py @@ -35,15 +35,16 @@ def generate_layers_prose(registry: Optional[LayerRegistry] = None) -> str: _LOOP = """One command does everything -- layers, extraction, enrichment, LLM route links, and embeddings: ```bash -tldrgraph init # interactive: asks once before enrichment token spend -tldrgraph init --yes # approve every current candidate until the campaign is done +tldrgraph init # agents auto-approve full runs; terminals may ask once +tldrgraph init --yes # non-agent explicit approval when confirmation asks for it ``` `init` automatically detects a supported agent CLI, uses 200-node enrichment batches, infers evidence-backed frontend/backend route links, and downloads/builds dense embeddings. It never guesses: when no agent is usable it prints a manual layer, enrichment, or route-link handoff. -Full approval is persisted across continuation runs. In a nested coding-agent session, +Full approval is persisted across continuation runs. In a coding-agent session, +plain `tldrgraph init` approves the full campaign and nested-agent protection means the host agent must read, answer, and apply every 200-node batch without asking again. `--batch 200` means all nodes in chunks; `--limit 200` means only 200 total. Never add `--limit`, `--no-llm-links`, or `--embeddings off` unless the user explicitly requests it. @@ -73,6 +74,8 @@ def generate_layers_prose(registry: Optional[LayerRegistry] = None) -> str: - **Complete `needs_llm_links` when shown.** Read `.tldrgraph/llm_links_request.yaml`, open the referenced frontend/backend files, and write `.tldrgraph/llm_links_response.yaml` with `{source, target, confidence, frontend_evidence, backend_evidence, explanation}`. + This is required for a complete init run unless the user explicitly requested + `--no-llm-links`. - **Continue after approval until `status: done`.** A `needs_enrichment` batch is work to process, not a reason to ask again. Do not add `--limit`, `--no-llm-links`, or `--embeddings off`.""" diff --git a/tldrgraph/visualizer/action_labels.py b/tldrgraph/visualizer/action_labels.py new file mode 100644 index 0000000..8497edb --- /dev/null +++ b/tldrgraph/visualizer/action_labels.py @@ -0,0 +1,81 @@ +"""Plain-language labels for workflow nodes.""" + +from __future__ import annotations + +import re +from typing import Any, Dict + +HTTP_VERBS = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"} +ACTION_WORDS = {"create", "update", "delete", "remove", "list", "fetch", "get", "load", "save"} + + +def _words(text: str) -> str: + cleaned = re.sub(r"\(.*?\)", "", str(text or "")).strip().split(".")[-1].lstrip("_") + cleaned = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", " ", cleaned).replace("_", " ").replace("-", " ") + return " ".join(w for w in cleaned.split() if w) + + +def humanize_symbol(text: str, default: str = "Do the work") -> str: + words = _words(text) + if not words: + return default + if words.lower().endswith(" page"): + words = "Show " + words[:-5] + elif not words.split()[0].lower() in ACTION_WORDS: + words = "Use " + words + return words[:1].upper() + words[1:] + + +def _usable_intent(text: str) -> str: + normalized = " ".join(str(text or "").split()) + if not normalized: + return "" + lower = normalized.lower() + bad = ( + lower.startswith(("the symbol ", "this symbol ", "core module symbol")), + " is defined in " in lower, + " located at " in lower, + "visible in that source module" in lower, + ) + if any(bad): + return "" + first = re.split(r"(?<=[.!?])\s+", normalized, 1)[0].strip(" .") + first = re.sub(r"^(This|The)\s+(function|method|class|component|route|endpoint|schema)\s+", "", first, flags=re.I) + first = re.sub(r"^(It|This)\s+", "", first, flags=re.I) + return first[:1].upper() + first[1:] if first else "" + + +def _route_action(label: str) -> str: + match = re.search(r"\b(" + "|".join(HTTP_VERBS) + r")\s+(/[^\s]+)", str(label or ""), re.I) + if not match: + return "" + method, path = match.group(1).upper(), match.group(2) + parts = [p for p in re.split(r"/+", path) if p and not p.startswith(":") and "{" not in p] + if not parts: + return "Handle web request" + last = parts[-1].lower() + resource = parts[-2] if last in ACTION_WORDS and len(parts) > 1 else parts[-1] + resource = _words(resource).lower().rstrip("s") or "item" + verb = last if last in ACTION_WORDS else { + "GET": "fetch", "POST": "create", "PUT": "update", "PATCH": "update", + "DELETE": "delete", + }.get(method, "handle") + action = f"{verb} {resource}".strip() + if "handler" in label.lower(): + action = "handle " + action + " request" + return action[:1].upper() + action[1:] + + +def action_label(node: Dict[str, Any], fallback: str = "") -> str: + intent = _usable_intent(str(node.get("intent") or node.get("summary") or "")) + if intent: + return intent + label = str(node.get("label") or node.get("symbol") or fallback or "") + route = _route_action(label) + if route: + return route + file_path = str(node.get("file") or "").lower() + layer = str(node.get("layer") or node.get("layer_id") or "").lower() + if ("schema.prisma" in file_path or "data" in layer or "persistence" in layer) and _words(label): + return f"Store {_words(label).lower()} data" + return humanize_symbol(label or fallback) diff --git a/tldrgraph/visualizer/assets/app.js b/tldrgraph/visualizer/assets/app.js index a238fc2..54485f1 100644 --- a/tldrgraph/visualizer/assets/app.js +++ b/tldrgraph/visualizer/assets/app.js @@ -1630,9 +1630,12 @@ function paintSymbolCard(c, n, opts) { c.font = 'bold 12px -apple-system, sans-serif'; c.fillText(truncateText(n.display_label || n.label, w - titleRight, c), left + 34, top + 18); - c.fillStyle = '#94a3b8'; - c.font = '10px monospace'; - c.fillText(truncateText(baseName(n.file), w - 74, c), left + 9, top + 38); + const metaText = o.metaText === undefined ? baseName(n.file) : o.metaText; + if (metaText) { + c.fillStyle = '#94a3b8'; + c.font = '10px monospace'; + c.fillText(truncateText(metaText, w - 74, c), left + 9, top + 38); + } c.textAlign = 'right'; c.font = '9px monospace'; @@ -2740,7 +2743,6 @@ function renderWorkflowsList() {
${escapeHtml(w.layer || 'Layer')} - ${escapeHtml(w.file)}
${escapeHtml(w.summary || '')}
${layerBadges}
@@ -3174,6 +3176,7 @@ function buildWorkflowLayout(w) { // happened to start with - a diamond on the line reads as a decision, and // the decisions belong underneath. const head = group.head; + const stepMeta = (w.steps || [])[group.step - 1] || {}; const bare = head.kind === 'start' || (head.kind === 'end' && group.members.length === 1); const lineNode = bare ? head : { ...head, @@ -3181,6 +3184,9 @@ function buildWorkflowLayout(w) { kind: 'step', label: head.step_title || head.label, detail: head.step_title ? head.label : head.detail, + node_id: stepMeta.node_id || head.node_id, + file: stepMeta.file || head.file, + line: stepMeta.code_start || head.line, isStepCard: true, }; const lineSize = shapeSize(lineNode); @@ -3374,6 +3380,7 @@ function drawRoundedTask(c, n, active, hovered) { hovered: hovered, emphasis: n.kind === 'step', dead: false, + metaText: '', badge: n.kind === 'step' && n.step ? ('#' + n.step) : (n.kind === 'loop' ? '\u21bb' : (n.external ? n.external : null)), }); @@ -3678,7 +3685,7 @@ function selectWorkflow(flowId) { } if (summaryEl) summaryEl.textContent = w.summary; if (stepsMetaEl) stepsMetaEl.textContent = `${w.step_count} Logical Steps`; - if (entryMetaEl) entryMetaEl.textContent = `Entry: ${w.root_node} (${w.file})`; + if (entryMetaEl) entryMetaEl.textContent = `Starts with: ${w.root_node || w.title}`; if (layersMetaEl) { layersMetaEl.innerHTML = (w.layers_involved || []).map(lname => { diff --git a/tldrgraph/visualizer/bpmn_data.py b/tldrgraph/visualizer/bpmn_data.py index e058a45..aea5cb2 100644 --- a/tldrgraph/visualizer/bpmn_data.py +++ b/tldrgraph/visualizer/bpmn_data.py @@ -1,13 +1,4 @@ -""" -Builds the BPMN payload the Workflow Explorer renders. - -Takes the deterministic control flow from :mod:`tldrgraph.bpmn_extract`, chains -one workflow's steps into a single process, resolves each activity to the graph -node it calls, sorts every element into a swimlane, and layers the business -phrasing over the top. The result is a diagram a non-engineer can read: named -activities, explicit decisions, and a visible boundary between what the tool -does by itself and what a person or an outside system does. -""" +"""Builds the plain-language BPMN payload the Workflow Explorer renders.""" from __future__ import annotations @@ -17,6 +8,7 @@ import networkx as nx from ..bpmn_extract import extract_process +from .action_labels import action_label, humanize_symbol from .bpmn_phrasing import ( branch_labels, load_project_phrases, @@ -43,24 +35,13 @@ USER_CALLS = {"prompt", "confirm", "input", "echo", "secho"} -def _humanize(text: str) -> str: - """Turns an identifier into a plain-language phrase as a last resort.""" - cleaned = re.sub(r"\(.*?\)", "", str(text or "")).strip() - cleaned = cleaned.split(".")[-1].lstrip("_") - cleaned = re.sub(r"(? str: if kind == "end": return "Finish and hand back the result" calls = raw.get("calls") or [] - return _humanize(calls[0]) if calls else _humanize(detail) + return humanize_symbol(calls[0]) if calls else humanize_symbol(detail) def _dedupe_flows(flows: List[Dict[str, Any]], valid: Set[str]) -> List[Dict[str, Any]]: diff --git a/tldrgraph/visualizer/flows_discover.py b/tldrgraph/visualizer/flows_discover.py index fdb1e7c..5054b79 100644 --- a/tldrgraph/visualizer/flows_discover.py +++ b/tldrgraph/visualizer/flows_discover.py @@ -12,13 +12,13 @@ from __future__ import annotations -import os import re from typing import Any, Callable, Dict, List, Optional, Set, Tuple import networkx as nx from ..flow_traversal import BRIDGE_RELATIONS +from .action_labels import action_label MAX_STEPS = 7 MIN_STEPS = 2 @@ -115,21 +115,6 @@ def _entry_score(node: Dict[str, Any]) -> Tuple[int, str]: return best, reason -def _humanize(text: str) -> str: - cleaned = re.sub(r"\(.*?\)", "", str(text or "")).strip().split(".")[-1].lstrip("_") - cleaned = re.sub(r"(? str: - base = os.path.basename(file_path or "") - return os.path.splitext(base)[0] or "app" - - def rank_entry_points( graph: nx.DiGraph, nodes_by_id: Dict[str, Dict[str, Any]], @@ -277,22 +262,21 @@ def discover_workflows( support = collect_support(steps) root_node = nodes_by_id[root] - title = _humanize(root_node.get("label") or root) - module = _module_of(root_node.get("file")) + title = action_label(root_node, root) workflows.append({ "id": "flow_found_" + re.sub(r"[^a-z0-9_]", "_", root.lower())[:60], - "title": f"{title} ({module})", + "title": title, "category": category, - "root_node": root_node.get("label") or root, + "root_node": title, + "root_symbol": root_node.get("label") or root, "root_id": root, "file": root_node.get("file") or "", "layer_id": root_node.get("layer_id") or "utility", "layer": root_node.get("layer") or "", "summary": ( - f"{category} beginning at {root_node.get('label') or root} in " - f"{root_node.get('file') or 'this repository'}, followed through " - f"{len(steps)} steps across {len(layers_involved)} layer(s)." + f"{category} for {title.lower()}, followed through " + f"{len(steps)} actions across {len(layers_involved)} layer(s)." ), "step_count": len(steps), "layers_involved": layers_involved, From c9ed22b0f4ea0e7e4bda9404b536bfd3dd8103ac Mon Sep 17 00:00:00 2001 From: Ashwani Kharwar Date: Thu, 10 Sep 2026 16:39:00 +0530 Subject: [PATCH 3/3] fix: highlight full code ranges in visualizer file viewer --- tests/test_visualizer.py | 2 ++ tldrgraph/visualizer/assets/app.js | 6 ++++- tldrgraph/visualizer/assets/sourceview.js | 28 ++++++++++++++++++----- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/tests/test_visualizer.py b/tests/test_visualizer.py index ab9cfd5..bd3260a 100644 --- a/tests/test_visualizer.py +++ b/tests/test_visualizer.py @@ -48,6 +48,8 @@ def test_generate_visualizer_html_file(mini_repo): assert "TLDRGraph" in content assert "findRouteRegistration" in content assert "route_path" in content + assert "normalizeFileHighlight" in content + assert "openFileViewer(hit.file, { start: start, end: end });" in content def test_payload_carries_source_pointers_not_source_text(mini_repo): diff --git a/tldrgraph/visualizer/assets/app.js b/tldrgraph/visualizer/assets/app.js index 54485f1..2abfd76 100644 --- a/tldrgraph/visualizer/assets/app.js +++ b/tldrgraph/visualizer/assets/app.js @@ -2928,7 +2928,11 @@ function initWorkflowCanvasEvents() { flowCanvas.addEventListener('dblclick', (ev) => { const pos = getMousePos(ev); const hit = flowHitTest(pos.x, pos.y); - if (hit && hit.file) openFileViewer(hit.file, hit.line || hit.code_start || 1); + if (hit && hit.file) { + const start = hit.line || hit.code_start || 1; + const end = hit.code_end && hit.code_end >= start ? hit.code_end : start; + openFileViewer(hit.file, { start: start, end: end }); + } }); } diff --git a/tldrgraph/visualizer/assets/sourceview.js b/tldrgraph/visualizer/assets/sourceview.js index 77e7009..039ba1c 100644 --- a/tldrgraph/visualizer/assets/sourceview.js +++ b/tldrgraph/visualizer/assets/sourceview.js @@ -476,6 +476,21 @@ function refreshOpenViews() { let fileViewerPath = null; let fileViewerHighlight = null; +function normalizeFileHighlight(highlight) { + if (!highlight) return null; + if (typeof highlight === 'number') { + return highlight > 0 ? { start: highlight, end: highlight } : null; + } + if (typeof highlight === 'string') { + const parsed = parseInt(highlight.replace(/^L/i, ''), 10); + return parsed > 0 ? { start: parsed, end: parsed } : null; + } + const start = parseInt(highlight.start || highlight.line || highlight.code_start, 10); + const end = parseInt(highlight.end || highlight.code_end || start, 10); + if (!start || start < 1) return null; + return { start: start, end: end && end >= start ? end : start }; +} + function fileSymbols(path) { const mod = DATA.modules.find(m => m.path === path); if (!mod) return []; @@ -486,8 +501,9 @@ function fileSymbols(path) { function openFileViewer(path, highlight) { if (!path) return; + const normalizedHighlight = normalizeFileHighlight(highlight); fileViewerPath = path; - fileViewerHighlight = highlight || null; + fileViewerHighlight = normalizedHighlight; const overlay = document.getElementById('file-viewer'); const body = document.getElementById('file-viewer-body'); @@ -501,7 +517,7 @@ function openFileViewer(path, highlight) { const openLink = document.getElementById('file-viewer-open'); openLink.href = 'vscode://file' + (DATA.root || '').replace(/\/$/, '') + '/' + path + - ':' + ((highlight && highlight.start) || 1); + ':' + ((normalizedHighlight && normalizedHighlight.start) || 1); if (accessMode === 'none') { body.innerHTML = '
' + @@ -523,8 +539,8 @@ function openFileViewer(path, highlight) { const language = (DATA.modules.find(m => m.path === path) || {}).language || 'plain'; const highlighted = highlightCode(entry.lines.join('\n'), language); - const from = highlight ? highlight.start : 0; - const to = highlight ? highlight.end : -1; + const from = normalizedHighlight ? normalizedHighlight.start : 0; + const to = normalizedHighlight ? normalizedHighlight.end : -1; document.getElementById('file-viewer-meta').textContent = entry.lines.length + ' lines - ' + language; @@ -542,7 +558,7 @@ function openFileViewer(path, highlight) { ? symbols.map(s => { const range = RANGE_CACHE.get(s.id); const line = (range && range.start) || s.code_start || 0; - const isActive = highlight && line === highlight.start; + const isActive = normalizedHighlight && line === normalizedHighlight.start; return '
' + '' + escapeHtml(s.display_label || s.label) + '' + @@ -551,7 +567,7 @@ function openFileViewer(path, highlight) { }).join('') : '
No indexed symbols in this file.
'; - if (highlight && highlight.start) scrollViewerToLine(highlight.start); + if (normalizedHighlight && normalizedHighlight.start) scrollViewerToLine(normalizedHighlight.start); }); }