From 8c318471acf55e403b4d7629f833098b8b86be96 Mon Sep 17 00:00:00 2001 From: ysun67 Date: Fri, 17 Jul 2026 15:49:34 -0400 Subject: [PATCH 1/2] Remove stale worktree gitlinks --- .claude/worktrees/agent-a0887fdee3510e86f | 1 - .claude/worktrees/agent-a0c836f04d21a7413 | 1 - .claude/worktrees/agent-a13563de836f67ac7 | 1 - .claude/worktrees/agent-a171f14d181124637 | 1 - .claude/worktrees/agent-a1a104366d617f22e | 1 - .claude/worktrees/agent-a1ab1cd2bbdd22d8d | 1 - .claude/worktrees/agent-a1c6b6d99c198e457 | 1 - .claude/worktrees/agent-a1d137564c62a6290 | 1 - .claude/worktrees/agent-a1d912191c716d37b | 1 - .claude/worktrees/agent-a24d347aeb8a1bae2 | 1 - .claude/worktrees/agent-a24d950c2b50b0ce0 | 1 - .claude/worktrees/agent-a26a54ab4027866f1 | 1 - .claude/worktrees/agent-a29c10e3a0bfc9f00 | 1 - .claude/worktrees/agent-a35bf8b3181762040 | 1 - .claude/worktrees/agent-a36ec61a3d0f2a3d3 | 1 - .claude/worktrees/agent-a3ac224bdbc08be50 | 1 - .claude/worktrees/agent-a3da655562484cfef | 1 - .claude/worktrees/agent-a3f1f8ebfd28fc03d | 1 - .claude/worktrees/agent-a42db4bf883cc0d1b | 1 - .claude/worktrees/agent-a480a1e0e9be42c82 | 1 - .claude/worktrees/agent-a49197e82edb42cc8 | 1 - .claude/worktrees/agent-a4de8737554ba6aa4 | 1 - .claude/worktrees/agent-a56c9650ee5a94158 | 1 - .claude/worktrees/agent-a6570c6d0a9f33cd3 | 1 - .claude/worktrees/agent-a6b667a17f179b0ad | 1 - .claude/worktrees/agent-a7287304fc234d7fe | 1 - .claude/worktrees/agent-a73b9012cf98ff7a7 | 1 - .claude/worktrees/agent-a79a02ee3033ccea4 | 1 - .claude/worktrees/agent-a7ddbc390758a3802 | 1 - .claude/worktrees/agent-a825517f95ce847ea | 1 - .claude/worktrees/agent-a88d447fa7a5a380c | 1 - .claude/worktrees/agent-a8f6a2a930f9cd270 | 1 - .claude/worktrees/agent-a95f48090051e7c41 | 1 - .claude/worktrees/agent-a9e8303f5bcaeced2 | 1 - .claude/worktrees/agent-a9ed629133905a183 | 1 - .claude/worktrees/agent-aa5e808d0ff68a7fd | 1 - .claude/worktrees/agent-ab05f854a47c99775 | 1 - .claude/worktrees/agent-abd1bd6eb13210b6c | 1 - .claude/worktrees/agent-abd6106a232fd1b99 | 1 - .claude/worktrees/agent-ac076bf38fdf880cf | 1 - .claude/worktrees/agent-ac68ee48a9d01178c | 1 - .claude/worktrees/agent-ac6b5738daabefe37 | 1 - .claude/worktrees/agent-acb48c7899359e1ae | 1 - .claude/worktrees/agent-ad34932b729e9987b | 1 - .claude/worktrees/agent-ad4270784f2327383 | 1 - .claude/worktrees/agent-ada998c6ce03e24ea | 1 - .claude/worktrees/agent-adb368fecfff1a049 | 1 - .claude/worktrees/agent-af33119eed489338e | 1 - .claude/worktrees/agent-af4fef22c344d4da6 | 1 - .claude/worktrees/agent-af540d582550cd7ef | 1 - .claude/worktrees/agent-afe05d0ca6f1080af | 1 - .claude/worktrees/pr-1c-target-kwarg | 1 - 52 files changed, 52 deletions(-) delete mode 160000 .claude/worktrees/agent-a0887fdee3510e86f delete mode 160000 .claude/worktrees/agent-a0c836f04d21a7413 delete mode 160000 .claude/worktrees/agent-a13563de836f67ac7 delete mode 160000 .claude/worktrees/agent-a171f14d181124637 delete mode 160000 .claude/worktrees/agent-a1a104366d617f22e delete mode 160000 .claude/worktrees/agent-a1ab1cd2bbdd22d8d delete mode 160000 .claude/worktrees/agent-a1c6b6d99c198e457 delete mode 160000 .claude/worktrees/agent-a1d137564c62a6290 delete mode 160000 .claude/worktrees/agent-a1d912191c716d37b delete mode 160000 .claude/worktrees/agent-a24d347aeb8a1bae2 delete mode 160000 .claude/worktrees/agent-a24d950c2b50b0ce0 delete mode 160000 .claude/worktrees/agent-a26a54ab4027866f1 delete mode 160000 .claude/worktrees/agent-a29c10e3a0bfc9f00 delete mode 160000 .claude/worktrees/agent-a35bf8b3181762040 delete mode 160000 .claude/worktrees/agent-a36ec61a3d0f2a3d3 delete mode 160000 .claude/worktrees/agent-a3ac224bdbc08be50 delete mode 160000 .claude/worktrees/agent-a3da655562484cfef delete mode 160000 .claude/worktrees/agent-a3f1f8ebfd28fc03d delete mode 160000 .claude/worktrees/agent-a42db4bf883cc0d1b delete mode 160000 .claude/worktrees/agent-a480a1e0e9be42c82 delete mode 160000 .claude/worktrees/agent-a49197e82edb42cc8 delete mode 160000 .claude/worktrees/agent-a4de8737554ba6aa4 delete mode 160000 .claude/worktrees/agent-a56c9650ee5a94158 delete mode 160000 .claude/worktrees/agent-a6570c6d0a9f33cd3 delete mode 160000 .claude/worktrees/agent-a6b667a17f179b0ad delete mode 160000 .claude/worktrees/agent-a7287304fc234d7fe delete mode 160000 .claude/worktrees/agent-a73b9012cf98ff7a7 delete mode 160000 .claude/worktrees/agent-a79a02ee3033ccea4 delete mode 160000 .claude/worktrees/agent-a7ddbc390758a3802 delete mode 160000 .claude/worktrees/agent-a825517f95ce847ea delete mode 160000 .claude/worktrees/agent-a88d447fa7a5a380c delete mode 160000 .claude/worktrees/agent-a8f6a2a930f9cd270 delete mode 160000 .claude/worktrees/agent-a95f48090051e7c41 delete mode 160000 .claude/worktrees/agent-a9e8303f5bcaeced2 delete mode 160000 .claude/worktrees/agent-a9ed629133905a183 delete mode 160000 .claude/worktrees/agent-aa5e808d0ff68a7fd delete mode 160000 .claude/worktrees/agent-ab05f854a47c99775 delete mode 160000 .claude/worktrees/agent-abd1bd6eb13210b6c delete mode 160000 .claude/worktrees/agent-abd6106a232fd1b99 delete mode 160000 .claude/worktrees/agent-ac076bf38fdf880cf delete mode 160000 .claude/worktrees/agent-ac68ee48a9d01178c delete mode 160000 .claude/worktrees/agent-ac6b5738daabefe37 delete mode 160000 .claude/worktrees/agent-acb48c7899359e1ae delete mode 160000 .claude/worktrees/agent-ad34932b729e9987b delete mode 160000 .claude/worktrees/agent-ad4270784f2327383 delete mode 160000 .claude/worktrees/agent-ada998c6ce03e24ea delete mode 160000 .claude/worktrees/agent-adb368fecfff1a049 delete mode 160000 .claude/worktrees/agent-af33119eed489338e delete mode 160000 .claude/worktrees/agent-af4fef22c344d4da6 delete mode 160000 .claude/worktrees/agent-af540d582550cd7ef delete mode 160000 .claude/worktrees/agent-afe05d0ca6f1080af delete mode 160000 .claude/worktrees/pr-1c-target-kwarg diff --git a/.claude/worktrees/agent-a0887fdee3510e86f b/.claude/worktrees/agent-a0887fdee3510e86f deleted file mode 160000 index 882ed10..0000000 --- a/.claude/worktrees/agent-a0887fdee3510e86f +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 882ed1078d34df13999fdcad05cc254c3ddac6eb diff --git a/.claude/worktrees/agent-a0c836f04d21a7413 b/.claude/worktrees/agent-a0c836f04d21a7413 deleted file mode 160000 index 0e8f751..0000000 --- a/.claude/worktrees/agent-a0c836f04d21a7413 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0e8f7519544fd44f66ff8a03a5a3baafc5fc7d5c diff --git a/.claude/worktrees/agent-a13563de836f67ac7 b/.claude/worktrees/agent-a13563de836f67ac7 deleted file mode 160000 index f9df620..0000000 --- a/.claude/worktrees/agent-a13563de836f67ac7 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f9df62006756acea434b14111543c7db92bf53cb diff --git a/.claude/worktrees/agent-a171f14d181124637 b/.claude/worktrees/agent-a171f14d181124637 deleted file mode 160000 index 3922846..0000000 --- a/.claude/worktrees/agent-a171f14d181124637 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 39228463b5d44e6dc51ecdb581a3d7920564aae3 diff --git a/.claude/worktrees/agent-a1a104366d617f22e b/.claude/worktrees/agent-a1a104366d617f22e deleted file mode 160000 index b83cf76..0000000 --- a/.claude/worktrees/agent-a1a104366d617f22e +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b83cf760f99ade36207fda4f0498745f230e5da1 diff --git a/.claude/worktrees/agent-a1ab1cd2bbdd22d8d b/.claude/worktrees/agent-a1ab1cd2bbdd22d8d deleted file mode 160000 index 27eb27c..0000000 --- a/.claude/worktrees/agent-a1ab1cd2bbdd22d8d +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 27eb27caa2b58462cce5030cda6502ff3ec6acc2 diff --git a/.claude/worktrees/agent-a1c6b6d99c198e457 b/.claude/worktrees/agent-a1c6b6d99c198e457 deleted file mode 160000 index e804035..0000000 --- a/.claude/worktrees/agent-a1c6b6d99c198e457 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e804035e6254b0eb93f54c94424ef7886fe4fd70 diff --git a/.claude/worktrees/agent-a1d137564c62a6290 b/.claude/worktrees/agent-a1d137564c62a6290 deleted file mode 160000 index f522d21..0000000 --- a/.claude/worktrees/agent-a1d137564c62a6290 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f522d21b61439d24734f731551770355732ca371 diff --git a/.claude/worktrees/agent-a1d912191c716d37b b/.claude/worktrees/agent-a1d912191c716d37b deleted file mode 160000 index c310407..0000000 --- a/.claude/worktrees/agent-a1d912191c716d37b +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c31040729342feb5414e9d9d29e959835af1c52c diff --git a/.claude/worktrees/agent-a24d347aeb8a1bae2 b/.claude/worktrees/agent-a24d347aeb8a1bae2 deleted file mode 160000 index c46b87c..0000000 --- a/.claude/worktrees/agent-a24d347aeb8a1bae2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c46b87ce309292c448e617a43ea19472557ff780 diff --git a/.claude/worktrees/agent-a24d950c2b50b0ce0 b/.claude/worktrees/agent-a24d950c2b50b0ce0 deleted file mode 160000 index 605dbca..0000000 --- a/.claude/worktrees/agent-a24d950c2b50b0ce0 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 605dbca01b1f072a557861b6c77ca9170c3c44e2 diff --git a/.claude/worktrees/agent-a26a54ab4027866f1 b/.claude/worktrees/agent-a26a54ab4027866f1 deleted file mode 160000 index 5be94d3..0000000 --- a/.claude/worktrees/agent-a26a54ab4027866f1 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5be94d35c376a667b465550ceccb4569815d556c diff --git a/.claude/worktrees/agent-a29c10e3a0bfc9f00 b/.claude/worktrees/agent-a29c10e3a0bfc9f00 deleted file mode 160000 index 34e224a..0000000 --- a/.claude/worktrees/agent-a29c10e3a0bfc9f00 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 34e224a6c80755557ce58feafaf138d18cd3ec69 diff --git a/.claude/worktrees/agent-a35bf8b3181762040 b/.claude/worktrees/agent-a35bf8b3181762040 deleted file mode 160000 index 2c6b32f..0000000 --- a/.claude/worktrees/agent-a35bf8b3181762040 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2c6b32fdb65a1d73929cda469ab84299419da97f diff --git a/.claude/worktrees/agent-a36ec61a3d0f2a3d3 b/.claude/worktrees/agent-a36ec61a3d0f2a3d3 deleted file mode 160000 index 8f41884..0000000 --- a/.claude/worktrees/agent-a36ec61a3d0f2a3d3 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8f41884a7053f6172f72a5d52fe985fc3ed57bf4 diff --git a/.claude/worktrees/agent-a3ac224bdbc08be50 b/.claude/worktrees/agent-a3ac224bdbc08be50 deleted file mode 160000 index 0ce01a0..0000000 --- a/.claude/worktrees/agent-a3ac224bdbc08be50 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0ce01a0dd188a3033f01182ea27f451dcacf9599 diff --git a/.claude/worktrees/agent-a3da655562484cfef b/.claude/worktrees/agent-a3da655562484cfef deleted file mode 160000 index 3829a14..0000000 --- a/.claude/worktrees/agent-a3da655562484cfef +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3829a14ebe9eecea01638b7795d3b85c25de252c diff --git a/.claude/worktrees/agent-a3f1f8ebfd28fc03d b/.claude/worktrees/agent-a3f1f8ebfd28fc03d deleted file mode 160000 index 5f8ad88..0000000 --- a/.claude/worktrees/agent-a3f1f8ebfd28fc03d +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5f8ad8881091629ceef5e7c36c22e43d11aca51e diff --git a/.claude/worktrees/agent-a42db4bf883cc0d1b b/.claude/worktrees/agent-a42db4bf883cc0d1b deleted file mode 160000 index b33d05c..0000000 --- a/.claude/worktrees/agent-a42db4bf883cc0d1b +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b33d05c72897d00ac9e5d6229e0d81980d4d00ad diff --git a/.claude/worktrees/agent-a480a1e0e9be42c82 b/.claude/worktrees/agent-a480a1e0e9be42c82 deleted file mode 160000 index c217d34..0000000 --- a/.claude/worktrees/agent-a480a1e0e9be42c82 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c217d341d2edc17a1ebf07ccceaabdb49636d06c diff --git a/.claude/worktrees/agent-a49197e82edb42cc8 b/.claude/worktrees/agent-a49197e82edb42cc8 deleted file mode 160000 index adfe381..0000000 --- a/.claude/worktrees/agent-a49197e82edb42cc8 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit adfe381e18fc828339d9d5b87c8b04b4e55a1fd9 diff --git a/.claude/worktrees/agent-a4de8737554ba6aa4 b/.claude/worktrees/agent-a4de8737554ba6aa4 deleted file mode 160000 index 5b2810c..0000000 --- a/.claude/worktrees/agent-a4de8737554ba6aa4 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5b2810ca1bef78c846eaa121deef8c407c53d8b6 diff --git a/.claude/worktrees/agent-a56c9650ee5a94158 b/.claude/worktrees/agent-a56c9650ee5a94158 deleted file mode 160000 index 5b28bdb..0000000 --- a/.claude/worktrees/agent-a56c9650ee5a94158 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5b28bdb5cea88d7f3f95730274ce0639f60211cb diff --git a/.claude/worktrees/agent-a6570c6d0a9f33cd3 b/.claude/worktrees/agent-a6570c6d0a9f33cd3 deleted file mode 160000 index f737eb1..0000000 --- a/.claude/worktrees/agent-a6570c6d0a9f33cd3 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f737eb115b06589516f3c530e6b15c221159b23f diff --git a/.claude/worktrees/agent-a6b667a17f179b0ad b/.claude/worktrees/agent-a6b667a17f179b0ad deleted file mode 160000 index 7e3892c..0000000 --- a/.claude/worktrees/agent-a6b667a17f179b0ad +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7e3892cb8ec5d75d9efaa206bf8d17a6be590641 diff --git a/.claude/worktrees/agent-a7287304fc234d7fe b/.claude/worktrees/agent-a7287304fc234d7fe deleted file mode 160000 index ef5003d..0000000 --- a/.claude/worktrees/agent-a7287304fc234d7fe +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ef5003d82a6d5a306d434ddf892b27f2d4c77e5a diff --git a/.claude/worktrees/agent-a73b9012cf98ff7a7 b/.claude/worktrees/agent-a73b9012cf98ff7a7 deleted file mode 160000 index fa01de8..0000000 --- a/.claude/worktrees/agent-a73b9012cf98ff7a7 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fa01de886b2392a37f671ee2655262f0f3d0a6df diff --git a/.claude/worktrees/agent-a79a02ee3033ccea4 b/.claude/worktrees/agent-a79a02ee3033ccea4 deleted file mode 160000 index c85cde8..0000000 --- a/.claude/worktrees/agent-a79a02ee3033ccea4 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c85cde8af33a9efc057b41eb8f864c0252dcbb3c diff --git a/.claude/worktrees/agent-a7ddbc390758a3802 b/.claude/worktrees/agent-a7ddbc390758a3802 deleted file mode 160000 index 3e726f0..0000000 --- a/.claude/worktrees/agent-a7ddbc390758a3802 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3e726f0dd5931c2f2944fdc474fc6c36503d95f1 diff --git a/.claude/worktrees/agent-a825517f95ce847ea b/.claude/worktrees/agent-a825517f95ce847ea deleted file mode 160000 index 7cf5212..0000000 --- a/.claude/worktrees/agent-a825517f95ce847ea +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7cf52126a37842ab4ae814196911559520be044f diff --git a/.claude/worktrees/agent-a88d447fa7a5a380c b/.claude/worktrees/agent-a88d447fa7a5a380c deleted file mode 160000 index e4497e4..0000000 --- a/.claude/worktrees/agent-a88d447fa7a5a380c +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e4497e46046c094fa1a08674ba887b17d0db3c19 diff --git a/.claude/worktrees/agent-a8f6a2a930f9cd270 b/.claude/worktrees/agent-a8f6a2a930f9cd270 deleted file mode 160000 index a3f1680..0000000 --- a/.claude/worktrees/agent-a8f6a2a930f9cd270 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a3f16806159faf037f1c759c91a30c424c40547b diff --git a/.claude/worktrees/agent-a95f48090051e7c41 b/.claude/worktrees/agent-a95f48090051e7c41 deleted file mode 160000 index 86c64f2..0000000 --- a/.claude/worktrees/agent-a95f48090051e7c41 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 86c64f2db7758d641f234d1d3215b5ff02b0d8b1 diff --git a/.claude/worktrees/agent-a9e8303f5bcaeced2 b/.claude/worktrees/agent-a9e8303f5bcaeced2 deleted file mode 160000 index 85bd4f6..0000000 --- a/.claude/worktrees/agent-a9e8303f5bcaeced2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 85bd4f657f95fd29d8e432295bf4b4d2da58dfb8 diff --git a/.claude/worktrees/agent-a9ed629133905a183 b/.claude/worktrees/agent-a9ed629133905a183 deleted file mode 160000 index 3f268f5..0000000 --- a/.claude/worktrees/agent-a9ed629133905a183 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3f268f5bc6e2fbd51c6fa42b6a9e4868a8a87cc8 diff --git a/.claude/worktrees/agent-aa5e808d0ff68a7fd b/.claude/worktrees/agent-aa5e808d0ff68a7fd deleted file mode 160000 index 981b0e4..0000000 --- a/.claude/worktrees/agent-aa5e808d0ff68a7fd +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 981b0e440b6c32c19f57b3b6bf391b57b7515a0f diff --git a/.claude/worktrees/agent-ab05f854a47c99775 b/.claude/worktrees/agent-ab05f854a47c99775 deleted file mode 160000 index a671976..0000000 --- a/.claude/worktrees/agent-ab05f854a47c99775 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a671976d24f491af5ccb7b5d1ab986678cff020f diff --git a/.claude/worktrees/agent-abd1bd6eb13210b6c b/.claude/worktrees/agent-abd1bd6eb13210b6c deleted file mode 160000 index e85f616..0000000 --- a/.claude/worktrees/agent-abd1bd6eb13210b6c +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e85f6163c141dfd342c220fb128dcfe55eb2661e diff --git a/.claude/worktrees/agent-abd6106a232fd1b99 b/.claude/worktrees/agent-abd6106a232fd1b99 deleted file mode 160000 index 7c3a726..0000000 --- a/.claude/worktrees/agent-abd6106a232fd1b99 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7c3a72657622b42a95caba30f204d97a4ebb2635 diff --git a/.claude/worktrees/agent-ac076bf38fdf880cf b/.claude/worktrees/agent-ac076bf38fdf880cf deleted file mode 160000 index 384d898..0000000 --- a/.claude/worktrees/agent-ac076bf38fdf880cf +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 384d898b2615b4554e01750ad8d66dbd203f5cd9 diff --git a/.claude/worktrees/agent-ac68ee48a9d01178c b/.claude/worktrees/agent-ac68ee48a9d01178c deleted file mode 160000 index 5d8f4a6..0000000 --- a/.claude/worktrees/agent-ac68ee48a9d01178c +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5d8f4a6c7a5ceb77382ef7e3583321676fc8b8b1 diff --git a/.claude/worktrees/agent-ac6b5738daabefe37 b/.claude/worktrees/agent-ac6b5738daabefe37 deleted file mode 160000 index b4db505..0000000 --- a/.claude/worktrees/agent-ac6b5738daabefe37 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b4db505f553c269dbda73d87f558c702f33288e7 diff --git a/.claude/worktrees/agent-acb48c7899359e1ae b/.claude/worktrees/agent-acb48c7899359e1ae deleted file mode 160000 index 4b57220..0000000 --- a/.claude/worktrees/agent-acb48c7899359e1ae +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4b5722003b94cd42e2b8bbac0297dcbbd93dec7e diff --git a/.claude/worktrees/agent-ad34932b729e9987b b/.claude/worktrees/agent-ad34932b729e9987b deleted file mode 160000 index 2b4f003..0000000 --- a/.claude/worktrees/agent-ad34932b729e9987b +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2b4f003215368f7b5b56859c8a7a61a9b43e7dc2 diff --git a/.claude/worktrees/agent-ad4270784f2327383 b/.claude/worktrees/agent-ad4270784f2327383 deleted file mode 160000 index 77b9a20..0000000 --- a/.claude/worktrees/agent-ad4270784f2327383 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 77b9a20eb3c89211c5f7343c2c915e168c3402a6 diff --git a/.claude/worktrees/agent-ada998c6ce03e24ea b/.claude/worktrees/agent-ada998c6ce03e24ea deleted file mode 160000 index 837de86..0000000 --- a/.claude/worktrees/agent-ada998c6ce03e24ea +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 837de86d71fdc32ee45db7b4c0e2518c3181ceae diff --git a/.claude/worktrees/agent-adb368fecfff1a049 b/.claude/worktrees/agent-adb368fecfff1a049 deleted file mode 160000 index ce85da1..0000000 --- a/.claude/worktrees/agent-adb368fecfff1a049 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ce85da147393882b99671a5a69d988cfeebd6dfd diff --git a/.claude/worktrees/agent-af33119eed489338e b/.claude/worktrees/agent-af33119eed489338e deleted file mode 160000 index 3ee3b3e..0000000 --- a/.claude/worktrees/agent-af33119eed489338e +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3ee3b3e3dec93820894e7050bf9cb48fac081d40 diff --git a/.claude/worktrees/agent-af4fef22c344d4da6 b/.claude/worktrees/agent-af4fef22c344d4da6 deleted file mode 160000 index b317aeb..0000000 --- a/.claude/worktrees/agent-af4fef22c344d4da6 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b317aebf1899e70d354d682b7d4777bfb1990cff diff --git a/.claude/worktrees/agent-af540d582550cd7ef b/.claude/worktrees/agent-af540d582550cd7ef deleted file mode 160000 index 1774267..0000000 --- a/.claude/worktrees/agent-af540d582550cd7ef +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 17742679cefd429f3d1590e330fffddf5da2d93a diff --git a/.claude/worktrees/agent-afe05d0ca6f1080af b/.claude/worktrees/agent-afe05d0ca6f1080af deleted file mode 160000 index f2e7cf2..0000000 --- a/.claude/worktrees/agent-afe05d0ca6f1080af +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f2e7cf2e3b4ad8b1d1fa73c2157c446944c26af1 diff --git a/.claude/worktrees/pr-1c-target-kwarg b/.claude/worktrees/pr-1c-target-kwarg deleted file mode 160000 index ac330c0..0000000 --- a/.claude/worktrees/pr-1c-target-kwarg +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ac330c02af5b38298caac541e8e0cdb677f41ecd From f82e54ef72436fdbc1df78cf42269cfb9c5d0bdc Mon Sep 17 00:00:00 2001 From: Colton Payne Date: Fri, 21 Aug 2026 11:19:54 -0400 Subject: [PATCH 2/2] Add VulReasoner phase-2 runner + trace-parity contract TRACE_PARITY_CONTRACT.md is the phase-2 equivalence spec (previously uncommitted, existed only on Colton's laptop and leibniz). run_vr_phase2.py runs one real trajectory's analyst fragment on SRDatalog per that contract: generalized ingest (per-trajectory hasLabel bounds, stepFrom windows, --kg / --end-time), native transitive closure for the kg_property analyst connectors, and per-rule witness relations emitting full qualified grounding sets (contract Tier 1). Conformance results live in vulreasoner-mindset branch hybrid-parity-harness (hybrid_e2e_check.py): Tier-1/Tier-2 and end-to-end composed-trace equality PASS on both pinned trajectories. Co-Authored-By: Claude Fable 5 --- minimal_vulreasoner/TRACE_PARITY_CONTRACT.md | 245 ++++++++++++ minimal_vulreasoner/run_vr_phase2.py | 389 +++++++++++++++++++ 2 files changed, 634 insertions(+) create mode 100644 minimal_vulreasoner/TRACE_PARITY_CONTRACT.md create mode 100644 minimal_vulreasoner/run_vr_phase2.py diff --git a/minimal_vulreasoner/TRACE_PARITY_CONTRACT.md b/minimal_vulreasoner/TRACE_PARITY_CONTRACT.md new file mode 100644 index 0000000..cb7e7c1 --- /dev/null +++ b/minimal_vulreasoner/TRACE_PARITY_CONTRACT.md @@ -0,0 +1,245 @@ +# Trace-parity contract (v1) + +**Scope:** the minimal VulReasoner subset. **Parties:** the PyReason oracle +(as consumed by VulReasoner's `trace_conversion.py` / `filter_reasoning_trace.py`) +and any SRDatalog implementation claiming trace parity (e.g. the +`srdatalog.pyreason` frontend of PR #2). + +This document defines *what "the SRDatalog trace matches PyReason" means* — +precisely enough to implement against and to test mechanically. It deliberately +does **not** require reproducing PyReason's `get_rule_trace` operation +chronology; §5 proves that within scope no chronology is needed. + +Field names below refer to VulReasoner's `ReasoningStep` +(`vulreasoner-mindset/src/trace_conversion.py`), the struct every downstream +consumer reads. Empirical claims about PyReason's CSV output were verified +against real traces in `vulreasoner-mindset/outputs/traces_api/CWE_121_MVP2/`. + +--- + +## 1. Scoped program fragment + +The contract covers exactly the programs expressible in +`minimal_vulreasoner/`: + +| Element | Allowed forms | +|---|---| +| Rules | the six analyst rules of `rules/analyst_rules.csv`: head `analystAt(CB2):paired_minimum_bounds_ann_fn`, delay `<-1`, body = `analystAt(CB1):[0.25,1]`, `hasLabel(CB1,·):[0.1,1]`, `hasLabel(CB2,·):[0.1,1]`, `connector(·,·):[0.1,1]`, `stepFrom(CB1,CB2)` | +| Annotation fn | `paired_minimum_bounds_ann_fn` only | +| Closed world | `analystAt` (fixed finite domain) | +| Facts | static `hasLabel(entity,label)`; timed `analystAt(seed)` on `[0,1]`; timed `stepFrom(bᵢ,bᵢ₊₁)` on `[i+1,i+2]`; KG attributes via `load_graphml` + `save_graph_attributes_to_trace` | +| Settings | `atom_trace=True`, `allow_ground_rules=True`, `save_graph_attributes_to_trace=True`, `persistent=False` (default) | +| Run | `pr.reason(timesteps=END_TIME)` | + +**Excluded** (out of contract, not merely untested): `<-0` rules (label +rules), the `closed_rule.csv` pair (`future`, negated-head +`inconsistent_rule`), `set_static` heads, binary heads with `infer_edges` +domain growth, the `obs` layer and ground-atom registration, `persistent=True`, +multi-KG orchestration, and `generalized_minimum_bounds_ann_fn`. Every one of +these invalidates at least one lemma in §5; extending the contract to them +requires a new version, not a relaxation. + +## 2. The canonical trace record + +Both engines must be renderable into a common form. One **record** corresponds +to one row of PyReason's rule-trace CSVs (one `ReasoningStep`): + +``` +Record = ( + time : int, # logical timestep + label : str, # predicate name + term : Node(id) | Edge(a, b), # 1-ary → Node, 2-ary → Edge + due_to : str, # rule name or fact name + triggered_by : "Fact" | "Rule", + consistent : bool, + clause_sets : tuple[frozenset[Term], ...], # per body-clause position; () for facts + inconsistency_message : None, # always None in scope +) +``` + +plus, per record, `old_bound` and `new_bound` (handled separately in §4 — +they are *not* part of the canonical identity because intermediate bounds are +order-sensitive when several updates hit one key). + +Canonicalization rules: + +- **term**: PyReason writes 1-ary atoms to the node CSV (`term = {"node": id}`) + and 2-ary atoms to the edge CSV (`term = {"edge": (a, b)}`). The SRDatalog + side must classify identically by predicate arity. +- **clause_sets**: parse each `Clause-i` cell with `ast.literal_eval`; map each + grounding through `grounding_to_term` semantics (bare string or 1-tuple → + Node, n-tuple → Edge); collect **as a set**, position-aligned with the + rule's body clauses in source order. Ordering *inside* a clause cell is + PyReason predicate-map admission order and is explicitly **not** part of the + contract. Facts have `clause_sets = ()`. +- **key** of a record group: `(label, term, time)` — this matches + `filter_reasoning_trace.index_steps` (note: that index ignores `time`; the + contract still groups by time because §3's multiset equality subsumes the + coarser index). + +## 3. Tier 1 — firing-multiset equality (REQUIRED) + +> The **multiset** of canonical records produced by the two engines over the +> full run must be equal. + +This is the load-bearing requirement. It implies, without further clauses: + +- `num_inferences` parity (it is `len(combined_trace)`), +- `input_valid` parity (§6; all records must have `consistent = True`), +- `base_filter` output parity — the filter's seeds (`due_to` prefix + `analyst-rule`), recursion gate (`triggered_by == "Rule"`), body-predicate + walk over `clause_sets`, and keep-index over `(label, term)` are all + functions of the canonical multiset only, +- secondary-filter (`label_filtering` / `workflow_filtering` / + `analyst_filtering`) parity, given Tier 2 below. + +The record multiset decomposes into two families, each independently checkable: + +**Fact records** — deterministic re-assertion schedule (verified against real +traces; a consequence of `persistent=False` worlds resetting to `[0,1]` each +step): + +| Source | Records emitted | +|---|---| +| static fact (incl. every `hasLabel` workflow fact) | one per `t ∈ [0, END_TIME]`, `due_to` = fact name | +| timed fact on `[t0, t1]` (`initial-control`, `workflow-edge-i`) | one per `t ∈ [t0, t1]`, `due_to` = fact name | +| GraphML attribute (KG nodes/edges, e.g. `can_cause`) | one per `t ∈ [0, END_TIME]`, `due_to = "graph-attribute-fact"` | + +All fact records: `triggered_by = "Fact"`, `clause_sets = ()`, +`consistent = True`. + +**Rule records** — by §5, within scope each `(rule, head-grounding, t)` fires +at most once, and its clause sets are the *complete* qualified-grounding sets +at `t`, uniquely determined by the inputs. So the expected rule-record multiset +is: for every analyst rule `r`, head grounding `h`, and timestep `t+1 ≤ +END_TIME` such that `r`'s body is satisfied at `t` (all five clauses have a +consistent grounding meeting their interval thresholds, `analystAt` read +closed-world), exactly one record + +``` +(t+1, "analystAt", Node(h), name(r), "Rule", True, + (Q₁, Q₂, Q₃, Q₄, Q₅)) +``` + +where `Qᵢ` is the full set of groundings of clause `i` qualified at `t` **in +the rule-consistent join** (the sets PyReason's `atom_trace` records: every +grounding that participates in some satisfying assignment, not only the +ARG-MAX winner). Recording only the winning witness is a contract violation — +`base_filter`'s transitive walk visits *every* grounding in every clause set. + +## 4. Tier 2 — bounds (REQUIRED, order-tolerant) + +Per key `(label, term, time)`: + +1. **Final bound — exact.** The last `new_bound` after all updates at that key + must be equal across engines. For `analystAt` keys this is precisely the + temporal interval map already validated by `validate_minimal_parity.py`. + For fact keys it is the fact's bound (`[1,1]`). +2. **Chain consistency — per engine (self-check).** Within one engine's + ordered records for a key: the first `old_bound` is the reset value + `[0,1]`; each subsequent `old_bound` equals the previous `new_bound`; the + last `new_bound` is the final bound. +3. **Interleaving — NOT compared.** When ≥ 2 records share a key (two analyst + rules deriving the same head at the same t), the sequence of intermediate + `old_bound → new_bound` transitions is engine-order-dependent and is + excluded from comparison. (Consequence of interval intersection being + commutative/associative: the final bound is order-free; the intermediates + are not.) No downstream consumer reads intermediates: `base_filter` never + touches bounds; `workflow_filtering` reads only `new_bound[1]` of + `stepFrom` records, which are single-record fact keys. +4. **Float equality** = equality of IEEE-754 float32 bit patterns + (`float32_to_u32`) after parsing; the PyReason side parses the CSV decimal + strings to float64 first. This matches the digest convention already used + by `benchmark_srdatalog.py`. + +The annotation semantics behind rule-record bounds (already implemented and +value-validated, restated here as the normative definition): per firing, each +satisfying body grounding is a witness; a witness's candidate interval is +`[min, min]-pairing` per `paired_minimum_bounds_ann_fn`; the firing's +contributed interval is that of the witness with maximal lower bound, ties +broken by **least admission rank** (PyReason first-match); the world update is +`old_bound ∩ contributed`. + +## 5. Why no chronology is needed (the snapshot-stability argument) + +The PR's conversion report declines to reproduce "callback trace chronology" +— same-snapshot witness groups and global `Fixed-Point-Operation` numbers. +Within the scoped fragment this problem dissolves: + +- **Lemma 1 (no intra-timestep feedback).** Every in-scope rule carries delay + `<-1`: bodies are evaluated against the state at `t`, heads land at `t+1`. + The state at `t` is fully determined before any rule evaluation at `t` + begins — it consists of fact assertions scheduled at `t` (§3 table) and + delayed heads computed from `t-1`. No in-scope derivation can change a set + another in-scope body reads at the same `t`. +- **Lemma 2 (stable qualified sets).** Therefore the qualified set `Qᵢ` of + every clause at `(rule, head, t)` is the unique fixpoint-independent set + determined by the inputs — there is no "growing snapshot" across + iterations. (The multi-row-per-key rows observed in full-VulReasoner traces + are caused by `<-0` label rules growing `hasLabel` within a timestep — + excluded from scope.) +- **Corollary.** Each `(rule, head, t)` yields at most one record, computable + without replaying PyReason's operational loop. The trace is a pure function + of the inputs. + +This is why `Fixed-Point-Operation` is excluded (§7): it is the only trace +column whose value is chronology, and nothing consumes it. + +## 6. Derived-output requirements + +For the per-KG result shape (`run_reasoning_for_kg`): + +| Field | Requirement | +|---|---| +| `input_valid` | `True` on both engines (no inconsistency sources in scope); any `consistent = False` record is a contract violation, not a tolerated difference | +| `num_inferences` | equal (follows from Tier 1) | +| `reasoning_trace` (base-filtered) | equal as canonical multisets (follows from Tiers 1–2) | +| `loaded_rules` | the six analyst rules, parsed per `parse_rules_from_file`, in file order — required because `base_filter` maps `due_to` → body predicates through it | +| `metrics`, `fixed_point_operation` | excluded (§7) | + +## 7. Explicitly out of contract + +- `Fixed-Point-Operation` values (SRDatalog may synthesize any int, e.g. `0` + or a per-time counter; must still be parseable as int). +- Row **order** in CSVs / JSON arrays, and ordering inside clause cells. + Comparison is multiset-based after canonicalization; byte-identical output + is a non-goal. (If byte-stable artifacts are wanted later, apply one shared + normalizer — sorted records, sorted clause sets — to *both* engines' + outputs; do not chase PyReason's native ordering.) +- Trace CSV filenames/timestamps, edge-vs-node file column counts, float + *formatting* (only float32 values are compared). +- Intermediate `old_bound → new_bound` interleavings on multi-record keys + (§4.3). +- Everything in §1 "Excluded". + +## 8. Conformance procedure + +A conforming implementation is verified by a differential harness +(`validate_trace_parity.py`, companion to `validate_minimal_parity.py`): + +1. **Oracle:** run the minimal example under PyReason, `pr.save_rule_trace`, + load CSVs with `trace_conversion` semantics, canonicalize per §2. +2. **Candidate:** run the SRDatalog implementation; emit records per §3 (fact + schedule from the input manifest; rule records from the materialized + candidate/selected relations plus clause-set queries), canonicalize per §2. +3. **Compare:** (a) Tier-1 multiset equality — report missing / extra records + grouped by key; (b) Tier-2 final-bound map equality under float32 bit + equality; (c) chain self-checks per engine; (d) run `base_filter` + + secondary filters over both canonical traces and assert equal kept + multisets — this guards the theorem-level claims in §3 empirically. +4. **Pass** = all of (a)–(d) on: the checked-in `CWE_121_MVP2` workflow, and + the deterministic stress workloads of `stress_workload.py` at ≥ 2 sizes. + +Divergence triage order: a Tier-1 mismatch on *fact* records is an ingest/ +schedule bug; on *rule* records with equal clause sets it is a threshold/CWA +read bug; a clause-set mismatch is a qualified-set (not winner-only) +materialization bug; a Tier-2-only mismatch is an annotation-fn or +intersection bug. + +## 9. Versioning + +This is **v1**, scoped to §1. Any widening — `<-0` rules, negation/ +`inconsistent_rule`, `set_static`, persistence, `obs` — must be a new +contract version with its own stability argument replacing §5 (Lemma 1 fails +for `<-0` rules and for the undelayed `inconsistent_rule`; conflict repair +additionally breaks §4.3's commutativity argument). diff --git a/minimal_vulreasoner/run_vr_phase2.py b/minimal_vulreasoner/run_vr_phase2.py new file mode 100644 index 0000000..e28ba70 --- /dev/null +++ b/minimal_vulreasoner/run_vr_phase2.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 +'''Run one real VulReasoner trajectory's phase-2 fragment on SRDatalog. + +Hybrid step-4 runner (see ~/Projects/HANDOFF.md and TRACE_PARITY_CONTRACT.md): +consumes a phase-2 input JSON extracted from a /reason capture +({initial_node, edges, end_time, has_label:[[entity,label,lo,hi],...]}), +the CWE_121_MVP2 GraphML, and the six analyst rules; runs the GPU fixpoint; +prints RESULT_JSON with: + - analyst_rows: the temporal analystAt map {node@time: [lower, upper]} + - witnesses: per analyst rule, every satisfying body grounding + [cb2, next_time, cb1, cause_label, effect_label] (full qualified sets per + contract Tier 1, not the ARG-MAX winner only) + - tc_facts: connector facts derived by native SRDatalog transitive closure + (kg_property_rules semantics: can_cause/contributes_to/derives, [1,1]) + +Connector transitivity is native SRDatalog TC via per-predicate Closed +relations; base connector facts are asserted [1,1] at ingest (true for this +KG), so TC rows inherit first-hop rank/bounds and still read [1,1]. + +Usage (from the srdatalog repo root): + uv run --project minimal_vulreasoner --frozen \ + python minimal_vulreasoner/run_vr_phase2.py --inputs t0.json [--no-compile] +''' + +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +from pathlib import Path + +REPO_SRC = Path(__file__).resolve().parents[1] / "src" +if str(REPO_SRC) not in sys.path: + sys.path.insert(0, str(REPO_SRC)) +HERE = Path(__file__).resolve().parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +import csv + +from srdatalog import ( + Program, + Relation, + Var, + build_project, + compile_jit_project, + float32_to_u32, + interval_lattice, + max_lower_lattice, +) +from srdatalog.dsl import Filter, Let +from stress_workload import decode_float32_bits + +from benchmark_srdatalog import _bind, _compiler_config +from example_config import KG_FILE, RULES_FILE +from graphml_ingest import parse_graphml +from srdatalog_query import _minimum3 +from analyst_rule_loader import load_analyst_rules + +import ctypes # noqa: E402 (after _bind import for parity with peers) + +# kg_property_rules.csv transitive predicates that are also analyst connectors. +TRANSITIVE_CONNECTORS = frozenset({"can_cause", "contributes_to", "derives"}) + + +def emit_trajectory_dataset(inputs: dict, output_dir: Path, rule_specs, + kg_file=KG_FILE) -> dict: + '''Emit the extensional relations for one real trajectory.''' + connector_predicates = tuple(spec.connector_predicate for spec in rule_specs) + graph = parse_graphml(kg_file, connector_predicates=connector_predicates) + bad = [c for c in graph.connectors if (c.lower, c.upper) != (1.0, 1.0)] + if bad: + raise ValueError( + f"TC inheritance assumes [1,1] base connector facts; found {bad[:3]}" + ) + + blocks = {inputs["initial_node"]} + for src, dst in inputs["edges"]: + blocks.add(src) + blocks.add(dst) + labels = {label for _, label, _, _ in inputs["has_label"]} + entities = {entity for entity, _, _, _ in inputs["has_label"]} + symbols = sorted(set(graph.nodes) | blocks | labels | entities) + symbol_ids = {symbol: index for index, symbol in enumerate(symbols)} + one = float32_to_u32(1.0) + + output_dir.mkdir(parents=True, exist_ok=True) + + def write(name, rows): + with (output_dir / name).open("w", newline="") as handle: + csv.writer(handle).writerows(rows) + + # PyReason initial-control Fact is valid on inclusive [0, 1] at [1,1]. + write("analyst_seed.csv", + ((symbol_ids[inputs["initial_node"]], t, one, one) for t in (0, 1))) + # Static hasLabel facts with augmenter bounds (timeless = static). + write("has_label.csv", + ((symbol_ids[e], symbol_ids[l], float32_to_u32(lo), float32_to_u32(hi)) + for e, l, lo, hi in inputs["has_label"])) + # PyReason asserts workflow edge i on inclusive times [i+1, i+2]. + step_rows = [] + for index, (src, dst) in enumerate(inputs["edges"]): + for t in (index + 1, index + 2): + step_rows.append((symbol_ids[src], symbol_ids[dst], t)) + write("step_from.csv", step_rows) + write("successor.csv", ((t, t + 1) for t in range(inputs["end_time"]))) + + by_predicate = {predicate: [] for predicate in connector_predicates} + for fact in graph.connectors: + by_predicate[fact.predicate].append(fact) + for predicate, facts in by_predicate.items(): + write(f"{predicate}.csv", + ((symbol_ids[f.source], symbol_ids[f.target], f.rank, + float32_to_u32(f.lower), float32_to_u32(f.upper)) for f in facts)) + + return { + "symbols": symbol_ids, + "graph_nodes": len(graph.nodes), + "graph_edges": graph.edge_count, + "selected_connector_facts": len(graph.connectors), + "base_connector_pairs": { + predicate: sorted({(f.source, f.target) for f in facts}) + for predicate, facts in by_predicate.items() + }, + } + + +def build_phase2_program(rule_specs) -> Program: + '''srdatalog_query.build_analyst_program plus native TC + witness outputs.''' + if any(spec.delay != 1 for spec in rule_specs): + raise ValueError("the explicit Successor encoding currently supports only <-1") + if any(spec.annotation_function != "paired_minimum_bounds_ann_fn" + for spec in rule_specs): + raise ValueError("unsupported analyst annotation function") + + analyst_seed = Relation("AnalystSeed", 4, column_types=(int,) * 4, + input_file="analyst_seed.csv") + has_label = Relation("HasLabel", 4, column_types=(int,) * 4, + input_file="has_label.csv") + step_from = Relation("StepFrom", 3, column_types=(int,) * 3, + input_file="step_from.csv") + successor = Relation("Successor", 2, column_types=(int,) * 2, + input_file="successor.csv") + analyst_at = Relation( + "AnalystAt", 4, column_types=(int,) * 4, print_size=True, + output_file="analyst_at.csv", + value_spec=interval_lattice(key_columns=(0, 1), lower_column=2, upper_column=3), + ) + + base_relations = {} + joined_relations = {} + rules = [] + tcx, tcy, tcz = Var("tcx"), Var("tcy"), Var("tcz") + tcr1, tcl1, tcu1 = Var("tcr1"), Var("tcl1"), Var("tcu1") + tcr2, tcl2, tcu2 = Var("tcr2"), Var("tcl2"), Var("tcu2") + for spec in rule_specs: + base = Relation(spec.relation_name, 5, column_types=(int,) * 5, + input_file=spec.input_file) + base_relations[spec.connector_predicate] = base + if spec.connector_predicate in TRANSITIVE_CONNECTORS: + closed = Relation(f"{spec.relation_name}Closed", 5, column_types=(int,) * 5, + print_size=True) + rules.append( + (closed(tcx, tcy, tcr1, tcl1, tcu1) + <= base(tcx, tcy, tcr1, tcl1, tcu1)).named(f"Copy{spec.relation_name}")) + # kg_property_rules transitivity; [1,1] inherited from the first hop + # (asserted at ingest), rank likewise — witness selection is unaffected + # because every derived interval is identical. + rules.append( + (closed(tcx, tcz, tcr1, tcl1, tcu1) + <= closed(tcx, tcy, tcr1, tcl1, tcu1) + & closed(tcy, tcz, tcr2, tcl2, tcu2)).named(f"TC{spec.relation_name}")) + joined_relations[spec.connector_predicate] = closed + else: + joined_relations[spec.connector_predicate] = base + + cb1, cb2 = Var("cb1"), Var("cb2") + time_, next_time = Var("time"), Var("next_time") + cause_label, effect_label = Var("cause_label"), Var("effect_label") + analyst_lower, analyst_upper = Var("analyst_lower"), Var("analyst_upper") + cause_lower, cause_upper = Var("cause_lower"), Var("cause_upper") + effect_lower, effect_upper = Var("effect_lower"), Var("effect_upper") + connector_lower, connector_upper = Var("connector_lower"), Var("connector_upper") + connector_rank = Var("connector_rank") + result_lower, result_upper = Var("result_lower"), Var("result_upper") + + rules.append( + (analyst_at(cb1, time_, analyst_lower, analyst_upper) + <= analyst_seed(cb1, time_, analyst_lower, analyst_upper)).named("AnalystSeed")) + + witness_relations = {} + for spec in rule_specs: + connector = joined_relations[spec.connector_predicate] + candidate = Relation( + f"{spec.relation_name}Candidate", 5, column_types=(int,) * 5, + value_spec=max_lower_lattice(key_columns=(0, 1), rank_column=2, + lower_column=3, upper_column=4), + ) + witness = Relation(f"{spec.relation_name}Witness", 5, column_types=(int,) * 5, + print_size=True) + witness_relations[spec.name] = witness + + def body(): + return ( + analyst_at(cb1, time_, analyst_lower, analyst_upper) + & has_label(cb1, cause_label, cause_lower, cause_upper) + & has_label(cb2, effect_label, effect_lower, effect_upper) + & connector(cause_label, effect_label, connector_rank, + connector_lower, connector_upper) + & step_from(cb1, cb2, time_) + & successor(time_, next_time) + & Filter( + vars=(analyst_lower.name, analyst_upper.name, + cause_lower.name, cause_upper.name, + effect_lower.name, effect_upper.name, + connector_lower.name, connector_upper.name), + code=( + f"return {analyst_lower.name} >= {float32_to_u32(spec.analyst_bound.lower)} && " + f"{analyst_upper.name} <= {float32_to_u32(spec.analyst_bound.upper)} && " + f"{cause_lower.name} >= {float32_to_u32(spec.cause_label_bound.lower)} && " + f"{cause_upper.name} <= {float32_to_u32(spec.cause_label_bound.upper)} && " + f"{effect_lower.name} >= {float32_to_u32(spec.effect_label_bound.lower)} && " + f"{effect_upper.name} <= {float32_to_u32(spec.effect_label_bound.upper)} && " + f"{connector_lower.name} >= {float32_to_u32(spec.connector_bound.lower)} && " + f"{connector_upper.name} <= {float32_to_u32(spec.connector_bound.upper)};" + ), + ) + & Let(result_lower.name, + _minimum3(cause_lower.name, effect_lower.name, connector_lower.name), + deps=(cause_lower.name, effect_lower.name, connector_lower.name)) + & Let(result_upper.name, + _minimum3(cause_upper.name, effect_upper.name, connector_upper.name), + deps=(cause_upper.name, effect_upper.name, connector_upper.name)) + ) + + rules.append( + (candidate(cb2, next_time, connector_rank, result_lower, result_upper) + <= body()).named(f"Analyst{spec.relation_name}")) + # Full qualified groundings for contract Tier 1 — every satisfying body + # assignment, independent of the candidate ARG-MAX selection. + rules.append( + (witness(cb2, next_time, cb1, cause_label, effect_label) + <= body()).named(f"Witness{spec.relation_name}")) + rules.append( + (analyst_at(cb2, next_time, result_lower, result_upper) + <= candidate(cb2, next_time, connector_rank, result_lower, result_upper) + ).named(f"Promote{spec.relation_name}Candidate")) + + return Program(rules=rules), witness_relations + + +def _copy_rows(lib, name: str, columns: int): + import cupy as cp + count = int(lib.srdatalog_dev_count(name.encode())) + host = [] + for column in range(columns): + pointer = int(lib.srdatalog_dev_ptr(name.encode(), column)) + memory = cp.cuda.UnownedMemory(pointer, count * 4, lib) + device = cp.ndarray((count,), dtype=cp.uint32, + memptr=cp.cuda.MemoryPointer(memory, 0)) + host.append(device.get()) + cp.cuda.get_current_stream().synchronize() + return [tuple(int(host[c][row]) for c in range(columns)) for row in range(count)] + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--inputs", required=True, help="phase-2 input JSON") + parser.add_argument("--kg", default=str(KG_FILE), + help="GraphML KG to ingest connectors from (default: the " + "minimal example's CWE_121_MVP2 copy)") + parser.add_argument("--end-time", type=int, default=None, + help="override the input JSON's end_time (sizes the " + "successor relation)") + parser.add_argument("--cache-base", default="./build") + parser.add_argument("--data-dir", default=None) + parser.add_argument("--jobs", type=int, default=8) + parser.add_argument("--no-compile", action="store_true") + parser.add_argument("--repeat", type=int, default=1, + help="extra srdatalog_run calls after the first (the " + "first pays GPU/RMM init; repeats measure cached " + "execution, same convention as benchmark_srdatalog)") + args = parser.parse_args() + + inputs = json.loads(Path(args.inputs).read_text()) + if args.end_time is not None: + inputs["end_time"] = args.end_time + rule_specs = load_analyst_rules(RULES_FILE) + data_dir = Path(args.data_dir or f"./build/vr_phase2_data_t{inputs.get('trajectory', 'x')}") + + ingest_started = time.perf_counter() + manifest = emit_trajectory_dataset(inputs, data_dir, rule_specs, + kg_file=Path(args.kg)) + ingest_seconds = time.perf_counter() - ingest_started + symbol_names = {sid: name for name, sid in manifest["symbols"].items()} + + program, witness_relations = build_phase2_program(rule_specs) + emit_started = time.perf_counter() + project = build_project(program, "VulReasonerPhase2Plan", cache_base=args.cache_base) + emit_seconds = time.perf_counter() - emit_started + + compile_seconds = 0.0 + if args.no_compile: + artifacts = list(Path(project["dir"]).glob("*.so")) + if not artifacts: + raise RuntimeError(f"no cached shared library in {project['dir']}; omit --no-compile") + artifact = str(artifacts[0].resolve()) + else: + started = time.perf_counter() + build = compile_jit_project(project, _compiler_config(args.jobs)) + compile_seconds = time.perf_counter() - started + if not build.ok(): + failure = next(r for r in build.compile_results if r.returncode) + raise RuntimeError((failure.stderr or failure.stdout)[-12000:]) + artifact = str(Path(build.artifact).resolve()) + + lib = _bind(artifact) + if lib.srdatalog_init() != 0: + raise RuntimeError("srdatalog_init failed") + load_started = time.perf_counter() + if lib.srdatalog_load_all(str(data_dir.resolve()).encode()) != 0: + raise RuntimeError("srdatalog_load_all failed") + load_seconds = time.perf_counter() - load_started + run_seconds_all = [] + for _ in range(max(1, args.repeat)): + run_started = time.perf_counter() + if lib.srdatalog_run(0) != 0: + raise RuntimeError("srdatalog_run failed") + run_seconds_all.append(time.perf_counter() - run_started) + run_seconds = run_seconds_all[0] + + raw = _copy_rows(lib, "AnalystAt", 4) + analyst_rows = dict(sorted( + (f"{symbol_names[node]}@{t}", [decode_float32_bits(lo), decode_float32_bits(hi)]) + for node, t, lo, hi in raw)) + + witnesses = {} + for rule_name, relation in witness_relations.items(): + rows = _copy_rows(lib, relation.name, 5) + witnesses[rule_name] = sorted( + [symbol_names[cb2], t, symbol_names[cb1], + symbol_names[cause], symbol_names[effect]] + for cb2, t, cb1, cause, effect in rows) + + tc_facts = {} + for spec in rule_specs: + if spec.connector_predicate not in TRANSITIVE_CONNECTORS: + continue + rows = _copy_rows(lib, f"{spec.relation_name}Closed", 5) + base_pairs = set(map(tuple, manifest["base_connector_pairs"][spec.connector_predicate])) + derived = sorted( + {(symbol_names[x], symbol_names[y]) for x, y, _, _, _ in rows} + - base_pairs) + tc_facts[spec.connector_predicate] = { + "derived_pairs": [list(p) for p in derived], + "derived_bounds": sorted({ + (decode_float32_bits(lo), decode_float32_bits(hi)) + for x, y, _, lo, hi in rows + if (symbol_names[x], symbol_names[y]) not in base_pairs}), + } + + result = { + "engine": "srdatalog", + "trajectory": inputs.get("trajectory"), + "graph_nodes": manifest["graph_nodes"], + "graph_edges": manifest["graph_edges"], + "selected_connector_facts": manifest["selected_connector_facts"], + "analyst_rows": analyst_rows, + "witnesses": witnesses, + "tc_facts": tc_facts, + "ingest_seconds": ingest_seconds, + "emit_seconds": emit_seconds, + "compile_seconds": compile_seconds, + "load_seconds": load_seconds, + "run_seconds": run_seconds, + "run_seconds_all": run_seconds_all, + } + print("RESULT_JSON=" + json.dumps(result, sort_keys=True), flush=True) + lib.srdatalog_shutdown() + sys.stdout.flush() + sys.stderr.flush() + # Same ctypes/CUDA teardown-order caveat as run_srdatalog_reasoner.py. + os._exit(0) + + +if __name__ == "__main__": + main()