diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..5279dd30f1c429da2e648958542f4df12a918cda 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +docs/images/david-demos.png filter=lfs diff=lfs merge=lfs -text +docs/images/david-evidence.png filter=lfs diff=lfs merge=lfs -text +docs/images/david-execution.png filter=lfs diff=lfs merge=lfs -text +docs/images/david-prolog.png filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..99d58929e0d7dac7d03d15b190c752e77121e480 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +.Python +.venv/ +venv/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..be3f7b28e564e7dd05eaf59d64adba1a4065ac0e --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..fe7f9139e1943995fbdfaa41693b7b1bb5918930 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +--- +license: agpl-3.0 +tags: +- snapkitty +- october-2026-drop +- j +- prolog +- postgresql +- pgvector +- python +- mixture-of-agents +- router +- agents +--- + +> Mirrored from [https://github.com/SNAPKITTYWEST/david](https://github.com/SNAPKITTYWEST/david) at commit `4ecbee9`. Part of the **SnapKitty October 2026 main drop**. + diff --git a/docs/contracts.md b/docs/contracts.md new file mode 100644 index 0000000000000000000000000000000000000000..5d513af7815f82ffc2fbe23d35e15131d254b4cf --- /dev/null +++ b/docs/contracts.md @@ -0,0 +1,45 @@ +# David contracts + +## WorkItem (agent input) + +``` +request_id, work_id, agent_id +task, evidence_bundle, required_outputs +permissions, deadline, risk_policy +``` + +## AgentResult (agent output) + +``` +agent_id +facts[], conclusions[], assumptions[], uncertainty[] +evidence_refs[], transformations[] +risk_signals[], provenance_root, status +``` + +## Evidence / EvidenceBundle + +See architecture §5. Every chunk carries source, hash, authority, and parent links — +agents never receive bare text without provenance. + +## Execution wiring + +`execution/` builds one WorkItem per selected `agent_id` and dispatches only that set. +Unselected agents (e.g. Quant on a COBOL migration query) never receive work. + +## Symbolic hard filters (`router/symbolic.py`) + +Predicates (not score penalties). An agent is eligible only if all three hold: + +- **capability_eligible** — `capabilities ∩ required_capabilities ≠ ∅`, or `control_plane` +- **permission_eligible** — `permissions ⊆ granted_permissions` +- **risk_eligible** — `risk_class` does not exceed `max_risk_class` (`low < medium < high < critical`) + +`apply_symbolic_filters` returns the agents that pass all three. +`minimum_agent_set` scores via `scorer.score_agent`, takes top-K, hard-filters, greedy-covers required capabilities, closes dependencies, then appends mandatory control-plane agents (`Risk`, `Response`). Pure dependency agents waive capability_eligible; after closure every agent must still pass permission + risk (else the parent needing a failing dep is dropped). Mandatory control plane skips the risk gate but still requires permission (same as scorer `include_control_plane`). + +## J execution (product) + +`j/execution.ijs` consumes the Prolog selected-set (one `agent_id` per line) and builds +WorkItems only for that set. `bundle_ok` requires each evidence row to carry non-empty +`evidence_id`, `source_id`, and `content_hash` — bare text is rejected. diff --git a/docs/images/.gitkeep b/docs/images/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/docs/images/david-demos.png b/docs/images/david-demos.png new file mode 100644 index 0000000000000000000000000000000000000000..cdb792bc363b05452336dc27d8577f00d71efdab --- /dev/null +++ b/docs/images/david-demos.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e546b51f7641bd4cc92acb9348a8d3b583600679a1ce0c61b4616bdc2c9405bd +size 109045 diff --git a/docs/images/david-evidence.png b/docs/images/david-evidence.png new file mode 100644 index 0000000000000000000000000000000000000000..cdb792bc363b05452336dc27d8577f00d71efdab --- /dev/null +++ b/docs/images/david-evidence.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e546b51f7641bd4cc92acb9348a8d3b583600679a1ce0c61b4616bdc2c9405bd +size 109045 diff --git a/docs/images/david-execution.png b/docs/images/david-execution.png new file mode 100644 index 0000000000000000000000000000000000000000..cdb792bc363b05452336dc27d8577f00d71efdab --- /dev/null +++ b/docs/images/david-execution.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e546b51f7641bd4cc92acb9348a8d3b583600679a1ce0c61b4616bdc2c9405bd +size 109045 diff --git a/docs/images/david-prolog.png b/docs/images/david-prolog.png new file mode 100644 index 0000000000000000000000000000000000000000..cdb792bc363b05452336dc27d8577f00d71efdab --- /dev/null +++ b/docs/images/david-prolog.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e546b51f7641bd4cc92acb9348a8d3b583600679a1ce0c61b4616bdc2c9405bd +size 109045 diff --git a/evidence/__init__.py b/evidence/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9f7acf3211189d7d017f75ea35d51790a065992c --- /dev/null +++ b/evidence/__init__.py @@ -0,0 +1,17 @@ +"""In-memory evidence retrieval (mirrors sql/001 hybrid sketch; no live DB).""" + +from .retrieve import ( + EvidenceRecord, + EvidenceIndex, + retrieve, + seed_cobol_corpus, + empty_bundle, +) + +__all__ = [ + "EvidenceRecord", + "EvidenceIndex", + "retrieve", + "seed_cobol_corpus", + "empty_bundle", +] diff --git a/evidence/retrieve.py b/evidence/retrieve.py new file mode 100644 index 0000000000000000000000000000000000000000..955315ad0753d01f7899769f6b1d6e2835c62796 --- /dev/null +++ b/evidence/retrieve.py @@ -0,0 +1,346 @@ +"""Hybrid evidence retrieval — dense + lexical + metadata + provenance. + +In-memory stand-in for sql/001_agent_and_evidence_indexes.sql. +Production SQL uses vector(1536); tests use fixed dim-8 vectors (no live DB). + +Merge / rerank rule (deterministic): + score = dense_cosine + lexical_overlap + source_authority - penalties + sort by (-score, evidence_id) + penalties: missing required parent_evidence when policy requires it + +Hybrid note: a chunk with weak dense but strong lexical can still rank +if lexical_overlap compensates (no hard dense floor). +""" + +from __future__ import annotations + +import hashlib +import math +import re +from dataclasses import dataclass, field +from typing import Any, Iterable, Sequence + + +# Production pgvector column is vector(1536); test corpus uses tiny fixed vectors. +EMBEDDING_DIM_TEST = 8 +EMBEDDING_DIM_PRODUCTION = 1536 + + +@dataclass +class EvidenceRecord: + """Fields mirror the SQL ``evidence`` table.""" + + evidence_id: str + source_id: str + source_type: str + document_id: str | None + location: str | None + content: str + content_hash: str + embedding: list[float] | None + source_authority: float = 0.5 + parent_evidence: tuple[str, ...] = () + created_at: str | None = None + + +def _content_hash(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16] + + +def _tokenize(text: str) -> set[str]: + return {t for t in re.findall(r"[a-z0-9]+", text.lower()) if len(t) > 1} + + +def cosine_similarity(a: Sequence[float], b: Sequence[float]) -> float: + if not a or not b or len(a) != len(b): + return 0.0 + dot = sum(x * y for x, y in zip(a, b)) + na = math.sqrt(sum(x * x for x in a)) + nb = math.sqrt(sum(y * y for y in b)) + if na == 0.0 or nb == 0.0: + return 0.0 + return dot / (na * nb) + + +def lexical_score(query: str, content: str) -> float: + """Token overlap stand-in for ``content_tsv @@ plainto_tsquery``.""" + q = _tokenize(query) + if not q: + return 0.0 + c = _tokenize(content) + if not c: + return 0.0 + return len(q & c) / len(q) + + +def empty_bundle() -> dict[str, Any]: + """EvidenceBundle-shaped dict with no hits (contracts.md).""" + return { + "facts": [], + "sources": [], + "provenance": [], + "retrieval_metadata": [], + } + + +def seed_cobol_corpus() -> list[EvidenceRecord]: + """Fixed COBOL-modernization corpus (dim-8 embeddings for tests). + + Axes (approx): [cobol, balance, payment, schema, pricing, vol, greek, junk] + """ + # Strong COBOL / balance / payment cluster + copybook = EvidenceRecord( + evidence_id="ev_copybook_01", + source_id="src_copybook_acct", + source_type="copybook", + document_id="ACCTREC.cpy", + location="01-ACCOUNT-BALANCE", + content=( + "01 ACCOUNT-RECORD. " + "05 ACCOUNT-BALANCE PIC S9(9)V99. " + "05 PAYMENT-AMOUNT PIC S9(7)V99. " + "05 LAST-PAYMENT-DATE PIC X(8)." + ), + content_hash="", + embedding=[0.95, 0.90, 0.85, 0.20, 0.05, 0.0, 0.0, 0.0], + source_authority=0.95, + parent_evidence=("ev_provenance_root",), + created_at="2026-01-01T00:00:00Z", + ) + cobol_src = EvidenceRecord( + evidence_id="ev_cobol_src_01", + source_id="src_cobol_pay", + source_type="cobol_source", + document_id="PAYROLL.cbl", + location="PROCEDURE DIVISION / CALC-BALANCE", + content=( + "COMPUTE ACCOUNT-BALANCE = ACCOUNT-BALANCE - PAYMENT-AMOUNT. " + "IF ACCOUNT-BALANCE < ZERO THEN PERFORM OVERDRAFT-CHECK. " + "MOVE PAYMENT-AMOUNT TO WS-LAST-PAYMENT." + ), + content_hash="", + embedding=[0.92, 0.88, 0.90, 0.15, 0.05, 0.0, 0.0, 0.0], + source_authority=0.90, + parent_evidence=("ev_provenance_root",), + created_at="2026-01-01T00:00:00Z", + ) + db2 = EvidenceRecord( + evidence_id="ev_db2_schema_01", + source_id="src_db2_acct", + source_type="db2_schema", + document_id="ACCT.DDL", + location="TABLE ACCOUNT_BALANCES", + content=( + "CREATE TABLE ACCOUNT_BALANCES (" + "ACCT_ID CHAR(12), BALANCE DECIMAL(11,2), " + "LAST_PAYMENT DECIMAL(9,2), UPDATED_TS TIMESTAMP)." + ), + content_hash="", + embedding=[0.70, 0.80, 0.55, 0.95, 0.05, 0.0, 0.0, 0.0], + source_authority=0.85, + parent_evidence=("ev_provenance_root",), + created_at="2026-01-01T00:00:00Z", + ) + # Should NOT win on a COBOL balance query (pricing / quant cluster) + quant = EvidenceRecord( + evidence_id="ev_quant_pricing_01", + source_id="src_quant_report", + source_type="quant_report", + document_id="PRICING_Q3.pdf", + location="section:monte_carlo", + content=( + "Monte Carlo pricing of exotic options. " + "Implied volatility surface and greek sensitivities. " + "No COBOL or account balance fields." + ), + content_hash="", + embedding=[0.05, 0.05, 0.05, 0.05, 0.95, 0.90, 0.85, 0.10], + source_authority=0.80, + parent_evidence=("ev_provenance_root",), + created_at="2026-01-01T00:00:00Z", + ) + # Low-authority junk for provenance filter tests + junk = EvidenceRecord( + evidence_id="ev_junk_lowauth_01", + source_id="src_web_scrape", + source_type="web_scrape", + document_id="random-blog.html", + location="body", + content=( + "Someone said COBOL payment balance might work like a spreadsheet. " + "Unverified forum post." + ), + content_hash="", + embedding=[0.60, 0.55, 0.50, 0.10, 0.10, 0.0, 0.0, 0.9], + source_authority=0.15, + parent_evidence=(), + created_at="2026-01-01T00:00:00Z", + ) + # Weak dense (far from COBOL cluster) but strong lexical on balance/payment/cobol + # — hybrid: lexical can lift it into results when dense alone would not. + lexical_lift = EvidenceRecord( + evidence_id="ev_lexical_lift_01", + source_id="src_glossary", + source_type="glossary", + document_id="LEGACY-GLOSSARY.txt", + location="entry:PAYMENT-BALANCE", + content=( + "cobol payment balance glossary entry: " + "PAYMENT and BALANCE fields on ACCOUNT records in legacy batch." + ), + content_hash="", + # Oriented away from COBOL cluster → weak cosine vs COBOL query + embedding=[0.10, 0.15, 0.10, 0.05, 0.40, 0.35, 0.30, 0.20], + source_authority=0.70, + parent_evidence=("ev_provenance_root",), + created_at="2026-01-01T00:00:00Z", + ) + + records = [copybook, cobol_src, db2, quant, junk, lexical_lift] + for r in records: + r.content_hash = _content_hash(r.content) + return records + + +# Canonical COBOL payment/balance query embedding (dim 8) +COBOL_BALANCE_QUERY = "cobol account payment balance CALC-BALANCE" +COBOL_BALANCE_QUERY_VEC = [0.93, 0.91, 0.88, 0.25, 0.05, 0.0, 0.0, 0.0] + + +@dataclass +class EvidenceIndex: + """In-memory evidence index (no Postgres required).""" + + records: list[EvidenceRecord] = field(default_factory=list) + + def __post_init__(self) -> None: + if not self.records: + self.records = seed_cobol_corpus() + self._by_id = {r.evidence_id: r for r in self.records} + + def add(self, record: EvidenceRecord) -> None: + if not record.content_hash: + record.content_hash = _content_hash(record.content) + self.records.append(record) + self._by_id[record.evidence_id] = record + + +def retrieve( + query: str, + *, + query_embedding: Sequence[float] | None = None, + index: EvidenceIndex | None = None, + source_types: Iterable[str] | None = None, + document_ids: Iterable[str] | None = None, + min_authority: float = 0.0, + require_parent_evidence: bool = False, + top_k: int = 10, +) -> dict[str, Any]: + """Hybrid retrieve → EvidenceBundle-shaped dict (facts/sources/provenance/…). + + Agents never receive bare text without provenance fields on each source. + """ + if not query or not query.strip(): + return empty_bundle() + + idx = index or EvidenceIndex() + st_filter = set(source_types) if source_types is not None else None + doc_filter = set(document_ids) if document_ids is not None else None + q_emb = list(query_embedding) if query_embedding is not None else None + + scored: list[tuple[float, EvidenceRecord, dict[str, float]]] = [] + for rec in idx.records: + # Metadata filters + if st_filter is not None and rec.source_type not in st_filter: + continue + if doc_filter is not None and rec.document_id not in doc_filter: + continue + # Provenance / authority + if rec.source_authority < min_authority: + continue + if require_parent_evidence and not rec.parent_evidence: + continue + + dense = 0.0 + if q_emb is not None and rec.embedding is not None: + dense = cosine_similarity(q_emb, rec.embedding) + lex = lexical_score(query, rec.content) + authority = float(rec.source_authority) + penalty = 0.0 + if require_parent_evidence and not rec.parent_evidence: + penalty += 1.0 # defensive; already filtered above + + score = dense + lex + authority - penalty + parts = { + "dense": dense, + "lexical": lex, + "source_authority": authority, + "penalty": penalty, + "score": score, + } + scored.append((score, rec, parts)) + + # Deterministic rerank: score desc, then evidence_id asc + scored.sort(key=lambda t: (-t[0], t[1].evidence_id)) + top = scored[:top_k] + + if not top: + return empty_bundle() + + facts: list[dict[str, Any]] = [] + sources: list[dict[str, Any]] = [] + provenance: list[dict[str, Any]] = [] + retrieval_metadata: list[dict[str, Any]] = [] + + for score, rec, parts in top: + facts.append( + { + "evidence_id": rec.evidence_id, + "summary": rec.content[:160], + "source_type": rec.source_type, + } + ) + sources.append( + { + "evidence_id": rec.evidence_id, + "source_id": rec.source_id, + "source_type": rec.source_type, + "document_id": rec.document_id, + "location": rec.location, + "content_hash": rec.content_hash, + "source_authority": rec.source_authority, + "parent_evidence": list(rec.parent_evidence), + "content": rec.content, + "retrieval_score": score, + } + ) + provenance.append( + { + "evidence_id": rec.evidence_id, + "source_id": rec.source_id, + "content_hash": rec.content_hash, + "parent_evidence": list(rec.parent_evidence), + "source_authority": rec.source_authority, + } + ) + retrieval_metadata.append( + { + "evidence_id": rec.evidence_id, + "dense": parts["dense"], + "lexical": parts["lexical"], + "source_authority": parts["source_authority"], + "penalty": parts["penalty"], + "score": parts["score"], + "embedding_dim_note": ( + f"test vectors dim={EMBEDDING_DIM_TEST}; " + f"production SQL uses vector({EMBEDDING_DIM_PRODUCTION})" + ), + } + ) + + return { + "facts": facts, + "sources": sources, + "provenance": provenance, + "retrieval_metadata": retrieval_metadata, + } diff --git a/evidence/test_retrieve.py b/evidence/test_retrieve.py new file mode 100644 index 0000000000000000000000000000000000000000..962e82a01c8a9d9553531e72de9253977750452b --- /dev/null +++ b/evidence/test_retrieve.py @@ -0,0 +1,160 @@ +"""Tests for in-memory hybrid evidence retrieval.""" + +from __future__ import annotations + +import unittest + +from evidence.retrieve import ( + COBOL_BALANCE_QUERY, + COBOL_BALANCE_QUERY_VEC, + EvidenceIndex, + empty_bundle, + lexical_score, + retrieve, + seed_cobol_corpus, +) + + +class RetrieveTests(unittest.TestCase): + def setUp(self): + self.index = EvidenceIndex(seed_cobol_corpus()) + self.query = COBOL_BALANCE_QUERY + self.qvec = COBOL_BALANCE_QUERY_VEC + + def test_cobol_balance_prefers_legacy_not_quant(self): + bundle = retrieve( + self.query, + query_embedding=self.qvec, + index=self.index, + min_authority=0.5, + require_parent_evidence=True, + top_k=5, + ) + ids = [s["evidence_id"] for s in bundle["sources"]] + self.assertTrue(ids, "expected non-empty sources") + # Top hit must not be the quant pricing chunk + self.assertNotEqual(ids[0], "ev_quant_pricing_01") + # COBOL / copybook / schema evidence should appear + self.assertTrue( + any( + i in ids + for i in ("ev_copybook_01", "ev_cobol_src_01", "ev_db2_schema_01") + ), + f"expected legacy evidence in {ids}", + ) + # Quant must not be the top hit; preferably absent from top-3 when + # authority + dense favor legacy chunks + top3 = ids[:3] + self.assertNotIn("ev_quant_pricing_01", top3) + + def test_provenance_authority_drops_junk(self): + # Without filter, junk may score on lexical overlap + open_bundle = retrieve( + self.query, + query_embedding=self.qvec, + index=self.index, + min_authority=0.0, + require_parent_evidence=False, + top_k=10, + ) + open_ids = {s["evidence_id"] for s in open_bundle["sources"]} + self.assertIn("ev_junk_lowauth_01", open_ids) + + filtered = retrieve( + self.query, + query_embedding=self.qvec, + index=self.index, + min_authority=0.5, + require_parent_evidence=True, + top_k=10, + ) + filt_ids = {s["evidence_id"] for s in filtered["sources"]} + self.assertNotIn("ev_junk_lowauth_01", filt_ids) + # Every returned source has provenance fields + for src in filtered["sources"]: + self.assertIn("content_hash", src) + self.assertIn("source_authority", src) + self.assertIn("parent_evidence", src) + self.assertTrue(src["parent_evidence"]) + + def test_hybrid_lexical_can_lift_weak_dense(self): + """Rule: score = dense + lexical + authority - penalties (no dense floor). + + ev_lexical_lift_01 has weak cosine vs the COBOL query vector but strong + token overlap on cobol/payment/balance, so it can appear in results. + """ + # Dense-only ranking of lexical_lift should be poor + from evidence.retrieve import cosine_similarity + + lift = self.index._by_id["ev_lexical_lift_01"] + dense_lift = cosine_similarity(self.qvec, lift.embedding) + dense_copy = cosine_similarity( + self.qvec, self.index._by_id["ev_copybook_01"].embedding + ) + self.assertLess(dense_lift, dense_copy) + self.assertLess(dense_lift, 0.55) # weak dense + + lex = lexical_score(self.query, lift.content) + self.assertGreaterEqual(lex, 0.5) # strong lexical + + bundle = retrieve( + self.query, + query_embedding=self.qvec, + index=self.index, + min_authority=0.5, + require_parent_evidence=True, + top_k=10, + ) + ids = [s["evidence_id"] for s in bundle["sources"]] + self.assertIn( + "ev_lexical_lift_01", + ids, + "lexical-strong / dense-weak chunk should appear under hybrid score", + ) + # Confirm retrieval_metadata records both components + meta = { + m["evidence_id"]: m for m in bundle["retrieval_metadata"] + } + self.assertIn("ev_lexical_lift_01", meta) + self.assertGreaterEqual(meta["ev_lexical_lift_01"]["lexical"], 0.5) + self.assertLess(meta["ev_lexical_lift_01"]["dense"], 0.55) + + def test_empty_query_returns_empty_bundle(self): + bundle = retrieve("", query_embedding=self.qvec, index=self.index) + self.assertEqual(bundle, empty_bundle()) + self.assertEqual(bundle["sources"], []) + self.assertEqual(bundle["facts"], []) + + def test_no_match_returns_empty_bundle(self): + # Metadata filter that matches nothing + bundle = retrieve( + self.query, + query_embedding=self.qvec, + index=self.index, + source_types=["nonexistent_type"], + top_k=5, + ) + self.assertEqual(bundle["sources"], []) + self.assertEqual(bundle["facts"], []) + self.assertEqual(bundle["provenance"], []) + self.assertEqual(bundle["retrieval_metadata"], []) + + def test_bundle_shape_has_provenance(self): + bundle = retrieve( + self.query, + query_embedding=self.qvec, + index=self.index, + min_authority=0.5, + top_k=3, + ) + for key in ("facts", "sources", "provenance", "retrieval_metadata"): + self.assertIn(key, bundle) + self.assertEqual(len(bundle["sources"]), len(bundle["provenance"])) + # No bare text: each source carries provenance fields + for src in bundle["sources"]: + self.assertTrue(src.get("content_hash")) + self.assertIsNotNone(src.get("source_authority")) + + +if __name__ == "__main__": + unittest.main() diff --git a/evidence/write_router_smoke.py b/evidence/write_router_smoke.py new file mode 100644 index 0000000000000000000000000000000000000000..83828e80274a0e5e52c61a727fd4c0c78daaa141 --- /dev/null +++ b/evidence/write_router_smoke.py @@ -0,0 +1,133 @@ +"""Append a router smoke log line using frozen scorer + Spec symbolic. + +Does not modify router/scorer.py or router/symbolic.py. +Scores come only from scorer.score_agent (via symbolic.minimum_agent_set). +""" + +from __future__ import annotations + +import sys +from datetime import datetime, timezone +from pathlib import Path + +# David root on path so `evidence` and `router` imports resolve +DAVID_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(DAVID_ROOT)) +sys.path.insert(0, str(DAVID_ROOT / "router")) + +from scorer import AgentDescriptor, RequestContext, score_agent # noqa: E402 +from symbolic import minimum_agent_set # noqa: E402 + +from evidence.retrieve import ( # noqa: E402 + COBOL_BALANCE_QUERY, + COBOL_BALANCE_QUERY_VEC, + EvidenceIndex, + retrieve, + seed_cobol_corpus, +) + +LOG_PATH = DAVID_ROOT / "logs" / "router_smoke.log" + + +def _agent(aid, caps, deps=(), control=False, risk="medium", perms=(), inputs=()): + """Same helper shape as router/test_scorer.py.""" + return AgentDescriptor( + agent_id=aid, + domain="test", + capabilities=frozenset(caps), + input_types=frozenset(inputs or ("source_code",)), + permissions=frozenset(perms or ("read_source",)), + risk_class=risk, + dependencies=frozenset(deps), + historical_success=0.5, + control_plane=control, + ) + + +def cobol_request_context(): + """COBOL-style RequestContext from router/test_scorer.py.""" + registry = { + "LegacyCobol": _agent( + "LegacyCobol", + ["cobol", "business_rule_extraction"], + deps=("Database", "Provenance"), + risk="high", + ), + "Database": _agent("Database", ["schema_analysis"], inputs=("db_schema",)), + "Provenance": _agent("Provenance", ["provenance_dag"]), + "Quant": _agent("Quant", ["pricing", "monte_carlo"]), + "Risk": _agent("Risk", ["risk_policy"], control=True, risk="critical"), + "Response": _agent("Response", ["synthesis"], control=True), + } + candidates = list(registry.values()) + req = RequestContext( + required_capabilities=frozenset({"cobol", "business_rule_extraction"}), + available_inputs=frozenset({"source_code", "copybooks"}), + granted_permissions=frozenset({"read_source", "read_copybooks"}), + max_risk_class="high", + semantic_scores={ + "LegacyCobol": 0.92, + "Database": 0.4, + "Quant": 0.1, + "Provenance": 0.3, + "Risk": 0.2, + "Response": 0.2, + }, + ) + return registry, candidates, req + + +def write_smoke_log(path: Path | None = None) -> Path: + log_path = path or LOG_PATH + log_path.parent.mkdir(parents=True, exist_ok=True) + + registry, candidates, req = cobol_request_context() + # Prefer Spec's minimum_agent_set (uses scorer.score_agent only) + selected = minimum_agent_set(candidates, req, registry) + # Touch score_agent explicitly so the smoke note is accurate + _ = {a.agent_id: score_agent(a, req) for a in candidates} + + bundle = retrieve( + COBOL_BALANCE_QUERY, + query_embedding=COBOL_BALANCE_QUERY_VEC, + index=EvidenceIndex(seed_cobol_corpus()), + min_authority=0.5, + require_parent_evidence=True, + top_k=5, + ) + top_eids = [s["evidence_id"] for s in bundle["sources"]] + + # Timestamp in America/Los_Angeles for operator-facing logs + try: + from zoneinfo import ZoneInfo + + ts = datetime.now(ZoneInfo("America/Los_Angeles")).strftime( + "%Y-%m-%d %H:%M:%S %Z" + ) + except Exception: + ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") + + quant_absent = "Quant" not in selected + risk_present = "Risk" in selected + response_present = "Response" in selected + + lines = [ + f"[{ts}] router_smoke minimum_agent_set selected={selected}", + f"[{ts}] Quant absent={quant_absent}; Risk present={risk_present}; " + f"Response present={response_present}", + f"[{ts}] note: scores came from scorer.score_agent only " + f"(via symbolic.minimum_agent_set; no second scorer)", + f"[{ts}] evidence_smoke query={COBOL_BALANCE_QUERY!r} " + f"top_evidence_ids={top_eids}", + "", + ] + + with log_path.open("a", encoding="utf-8") as fh: + fh.write("\n".join(lines)) + + return log_path + + +if __name__ == "__main__": + out = write_smoke_log() + print(f"appended router smoke log → {out}") diff --git a/execution/__init__.py b/execution/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1347f774ddf9daa490986f1beb8689ab5a0da050 --- /dev/null +++ b/execution/__init__.py @@ -0,0 +1,20 @@ +"""Sparse agent execution: WorkItem in, AgentResult out, selected set only.""" + +from .dispatcher import ( + ExecutionRequest, + build_work_items, + dispatch, + stub_handler, +) +from .models import AgentResult, Evidence, EvidenceBundle, WorkItem + +__all__ = [ + "AgentResult", + "Evidence", + "EvidenceBundle", + "ExecutionRequest", + "WorkItem", + "build_work_items", + "dispatch", + "stub_handler", +] diff --git a/execution/dispatcher.py b/execution/dispatcher.py new file mode 100644 index 0000000000000000000000000000000000000000..ede7f8ae36bf878287bb1965808cfc60b69e8ae9 --- /dev/null +++ b/execution/dispatcher.py @@ -0,0 +1,99 @@ +"""Execute only the sparse-selected agent set — no swarm fan-out.""" + +from __future__ import annotations + +import uuid +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass + +from .models import AgentResult, EvidenceBundle, WorkItem + +# Handler: (WorkItem) -> AgentResult +AgentHandler = Callable[[WorkItem], AgentResult] + + +@dataclass(frozen=True) +class ExecutionRequest: + request_id: str + task: str + evidence_bundle: EvidenceBundle + required_outputs: tuple[str, ...] + permissions: frozenset[str] + risk_policy: str + deadline: str | None = None + + +def build_work_items( + selected_agent_ids: Sequence[str], + req: ExecutionRequest, +) -> list[WorkItem]: + """One WorkItem per selected agent_id. Order preserved. No extras.""" + if not selected_agent_ids: + return [] + # Deduplicate while preserving order (router may close deps + control plane). + seen: set[str] = set() + ordered: list[str] = [] + for aid in selected_agent_ids: + if aid not in seen: + seen.add(aid) + ordered.append(aid) + return [ + WorkItem( + request_id=req.request_id, + work_id=f"{req.request_id}:{aid}:{uuid.uuid4().hex[:8]}", + agent_id=aid, + task=req.task, + evidence_bundle=req.evidence_bundle, + required_outputs=req.required_outputs, + permissions=req.permissions, + deadline=req.deadline, + risk_policy=req.risk_policy, + ) + for aid in ordered + ] + + +def dispatch( + selected_agent_ids: Sequence[str], + req: ExecutionRequest, + handlers: Mapping[str, AgentHandler], + *, + allow_missing: bool = False, +) -> list[AgentResult]: + """ + Run handlers only for selected agents. + + Agents not in selected_agent_ids are never invoked. + Missing handlers raise KeyError unless allow_missing (then status=skipped). + """ + items = build_work_items(selected_agent_ids, req) + results: list[AgentResult] = [] + for item in items: + handler = handlers.get(item.agent_id) + if handler is None: + if not allow_missing: + raise KeyError(f"no handler for selected agent {item.agent_id!r}") + results.append( + AgentResult(agent_id=item.agent_id, status="skipped") + ) + continue + results.append(handler(item)) + return results + + +def stub_handler(agent_id: str) -> AgentHandler: + """Deterministic stub for wiring tests (not a real agent).""" + + def _run(item: WorkItem) -> AgentResult: + assert item.agent_id == agent_id + return AgentResult( + agent_id=agent_id, + conclusions=({"note": f"stub:{agent_id}", "work_id": item.work_id},), + evidence_refs=tuple( + e.evidence_id for e in item.evidence_bundle.sources + ), + provenance_root=f"prov:{item.request_id}:{agent_id}", + status="ok", + ) + + return _run diff --git a/execution/models.py b/execution/models.py new file mode 100644 index 0000000000000000000000000000000000000000..66795a23491e656c252c60dd0ca6201c653aa965 --- /dev/null +++ b/execution/models.py @@ -0,0 +1,66 @@ +"""WorkItem / AgentResult / EvidenceBundle contracts for sparse execution.""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass, field +from typing import Any + + +@dataclass(frozen=True) +class Evidence: + evidence_id: str + source_id: str + source_type: str + document_id: str + location: str + content_hash: str + embedding_id: str | None = None + retrieved_by: str | None = None + retrieval_score: float | None = None + source_authority: str | None = None + timestamp: str | None = None + parent_evidence: tuple[str, ...] = () + content: str | None = None # never delivered without the fields above + + +@dataclass(frozen=True) +class EvidenceBundle: + facts: tuple[dict[str, Any], ...] = () + sources: tuple[Evidence, ...] = () + provenance: tuple[dict[str, Any], ...] = () + retrieval_metadata: tuple[dict[str, Any], ...] = () + + +@dataclass(frozen=True) +class WorkItem: + request_id: str + work_id: str + agent_id: str + task: str + evidence_bundle: EvidenceBundle + required_outputs: tuple[str, ...] + permissions: frozenset[str] + deadline: str | None + risk_policy: str + + def to_dict(self) -> dict[str, Any]: + d = asdict(self) + d["permissions"] = sorted(self.permissions) + return d + + +@dataclass(frozen=True) +class AgentResult: + agent_id: str + facts: tuple[dict[str, Any], ...] = () + conclusions: tuple[dict[str, Any], ...] = () + assumptions: tuple[str, ...] = () + uncertainty: tuple[str, ...] = () + evidence_refs: tuple[str, ...] = () + transformations: tuple[dict[str, Any], ...] = () + risk_signals: tuple[str, ...] = () + provenance_root: str | None = None + status: str = "ok" # ok | failed | escalated | skipped + + def to_dict(self) -> dict[str, Any]: + return asdict(self) diff --git a/execution/test_dispatcher.py b/execution/test_dispatcher.py new file mode 100644 index 0000000000000000000000000000000000000000..3cec401b1b48e080be4a8b47a7a41598d14b2a7f --- /dev/null +++ b/execution/test_dispatcher.py @@ -0,0 +1,97 @@ +"""Smoke: only selected agents receive WorkItems and run.""" + +from __future__ import annotations + +import unittest + +from execution.models import Evidence, EvidenceBundle +from execution.dispatcher import ( + ExecutionRequest, + build_work_items, + dispatch, + stub_handler, +) + + +SELECTED = [ + "LegacyCobol", + "Database", + "ReverseEngineering", + "Provenance", + "Risk", + "Response", +] + + +class TestSparseDispatch(unittest.TestCase): + def setUp(self) -> None: + self.bundle = EvidenceBundle( + sources=( + Evidence( + evidence_id="e1", + source_id="s1", + source_type="cobol_source", + document_id="pay.cbl", + location="lines:100-200", + content_hash="abc", + content="COMPUTE BALANCE ...", + ), + ), + ) + self.req = ExecutionRequest( + request_id="req-cobol-1", + task=( + "Explain how this COBOL payment program calculates " + "the account balance and determine whether its Java " + "replacement preserves the behavior." + ), + evidence_bundle=self.bundle, + required_outputs=("business_rules", "equivalence_result"), + permissions=frozenset( + {"read_source", "read_copybooks", "read_schema", "read_evidence"} + ), + risk_policy="escalate_on_high", + ) + self.handlers = {aid: stub_handler(aid) for aid in SELECTED} + # Quant must never run even if a handler exists. + self.handlers["Quant"] = stub_handler("Quant") + + def test_work_items_only_for_selected(self) -> None: + items = build_work_items(SELECTED, self.req) + self.assertEqual([i.agent_id for i in items], SELECTED) + self.assertTrue(all(i.request_id == "req-cobol-1" for i in items)) + self.assertNotIn("Quant", [i.agent_id for i in items]) + + def test_dispatch_invokes_only_selected(self) -> None: + invoked: list[str] = [] + + def wrap(aid: str): + inner = stub_handler(aid) + + def _h(item): + invoked.append(aid) + return inner(item) + + return _h + + handlers = {aid: wrap(aid) for aid in self.handlers} + results = dispatch(SELECTED, self.req, handlers) + self.assertEqual([r.agent_id for r in results], SELECTED) + self.assertEqual(invoked, SELECTED) + self.assertNotIn("Quant", invoked) + self.assertTrue(all(r.status == "ok" for r in results)) + + def test_empty_selection_runs_nobody(self) -> None: + results = dispatch([], self.req, self.handlers) + self.assertEqual(results, []) + + def test_evidence_bundle_not_bare_text(self) -> None: + items = build_work_items(["LegacyCobol"], self.req) + src = items[0].evidence_bundle.sources[0] + self.assertEqual(src.evidence_id, "e1") + self.assertEqual(src.content_hash, "abc") + self.assertIsNotNone(src.source_id) + + +if __name__ == "__main__": + unittest.main() diff --git a/j/README.md b/j/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a2669339a7ff5980bd91a355c50dfee0eb4e3fa8 --- /dev/null +++ b/j/README.md @@ -0,0 +1,26 @@ +# David core (J) + +Stack lock: **J + PostgreSQL/pgvector + Prolog**. + +| Path | Role | +|------|------| +| `scorer.ijs` | Deterministic score shape (ported from Python harness) | +| `eligibility.ijs` | **Product** — J → SWI-Prolog eligibility (candidates + request → minimum agent set) | +| `run_eligibility_smoke.ijs` | Product smoke for `eligibility.ijs` (ANN fixture + Quant negative) | +| `symbolic_call.ijs` | Thin delegate → `run_eligibility_smoke.ijs` | +| `execution.ijs` | WorkItem → AgentResult; Prolog selected-set; EvidenceBundle-only | +| `run_execution_smoke.ijs` | Smoke for `execution.ijs` | +| `evidence.ijs` | Hybrid evidence retrieve (product; seed + `ev_retrieve` / `ev_smoke`) | +| `run_evidence_smoke.ijs` | Smoke for `evidence.ijs` | +| `fixtures/ann_topk.txt` | Fixture candidate ids (ANN top-K shape + Quant negative) | +| `../sql/001_agent_and_evidence_indexes.sql` | AGENT / EVIDENCE indexes | +| `../sql/002_evidence_hybrid_retrieve.sql` | Product hybrid retrieve against `evidence` | +| `../router/symbolic_rules.pl` | Product Prolog eligibility rules | +| `../router/registry_facts.pl` | Generated agent facts from `registry/seed_agents.yaml` | + +Python under `../router`, `../execution`, `../evidence` is smoke only — not the product. + +Foundry J lives separately at `/workspace/foundry-j`. + +Handoff: `../scripts/prolog_selected_handoff.sh` writes `../logs/selected_cobol.txt` for `load_selected`. +Product Spec path: `eligibility.ijs` → `swipl` `run_eligibility/1` (not Python filters). diff --git a/j/eligibility.ijs b/j/eligibility.ijs new file mode 100644 index 0000000000000000000000000000000000000000..3a2b2d3a332f8b05676b3fe536ddff0ff8f25a7f --- /dev/null +++ b/j/eligibility.ijs @@ -0,0 +1,93 @@ +NB. SPDX-License-Identifier: AGPL-3.0-only +NB. Product path: J passes candidates + request into SWI-Prolog eligibility. +NB. Embeddings propose; Prolog decides. No Python in this path. +NB. Atom map: registry id → Prolog atom = downcase first letter (LegacyCobol→legacyCobol). + +cocurrent 'david' + +NB. Repo root relative to j/ working directory +DAVID_ROOT=: '..' + +NB. Registry PascalCase id → Prolog atom (first char only) +to_prolog_atom=: 3 : 0 + s=. ,": y + if. 0=#s do. '' return. end. + u=. a. i. {. s + if. (u >: 65) *. (u <: 90) do. + ((u + 32) { a.) , }. s + else. + s + end. +) + +NB. Write temporary Prolog request facts (plain clauses; preds are dynamic in rules). +NB. y = (c), ').') + end. + for_p. perms do. + lines=. lines , <('granted_permission(', req, ', ', (>p), ').') + end. + lines=. lines , <('max_risk_class(', req, ', ', maxr, ').') + for_a. cands do. + atom=. to_prolog_atom >a + lines=. lines , <('ann_candidate(', req, ', ', atom, ').') + end. + (; LF ,~ each lines) 1!:2 L + if. (0=#s) +. '#' = {. s , ' ' do. continue. end. + keep=. keep , (x { y)' + +NB. Open one level if boxed +ev_unbox1 =: 3 : 'if. 32 = 3!:0 y do. > y else. y end.' + +NB. --- lowercase + alphanumeric tokens len>1 (lexical stand-in for tsv) ------- +EV_AZ =: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +EV_az =: 'abcdefghijklmnopqrstuvwxyz' +EV_alnum =: EV_az , '0123456789' + +ev_lc1 =: 3 : 0 + i =. EV_AZ i. y + if. i < #EV_AZ do. i { EV_az else. y end. +) + +ev_lc =: 3 : 'ev_lc1"0 y' + +ev_tokens =: 3 : 0 + if. 0 = #y do. 0 $ a: return. end. + s =. ev_lc , y + ok =. s e. EV_alnum + s =. ok } (' ' #~ #s) ,: s + parts =. <;._1 ' ' , s + parts =. parts #~ 1 < #&> parts + ~. parts +) + +NB. lexical overlap = |q ∩ c| / |q| (0 if empty query tokens) +ev_lexical =: 4 : 0 + q =. ev_tokens x + if. 0 = #q do. 0.0 return. end. + c =. ev_tokens y + if. 0 = #c do. 0.0 return. end. + (# q -. q -. c) % #q +) + +NB. cosine similarity of two numeric lists +ev_cosine =: 4 : 0 + a =. , x + b =. , y + if. (#a) ~: #b do. 0.0 return. end. + if. (0 = #a) +. (0 = #b) do. 0.0 return. end. + na =. %: +/ *: a + nb =. %: +/ *: b + if. (0 = na) +. (0 = nb) do. 0.0 return. end. + (+/ a * b) % (na * nb) +) + +NB. Axes for dim-8 smoke query embed (prod = vector(1536) via encoder) +NB. [cobol, balance, payment, schema, pricing, vol, greek, junk] +EV_AXES =: 'cobol';'balance';'payment';'schema';'pricing';'vol';'greek';'junk' + +ev_qembed =: 3 : 0 + toks =. ev_tokens y + NB. synonym: volatility → vol + if. (<'volatility') e. toks do. toks =. toks , <'vol' end. + v =. 0 #~ #EV_AXES + i =. 0 + while. i < #EV_AXES do. + if. (i { EV_AXES) e. toks do. v =. 1 i} v end. + i =. i + 1 + end. + NB. fallback canned COBOL qvec when axis hits empty but query non-blank + if. (0 = +/ v) *. (0 < #y) do. + v =. 0.93 0.91 0.88 0.25 0.05 0 0 0 + end. + v +) + +NB. --- seed corpus (mirrors sql/003 + Python harness columns) ----------------- +NB. row: id ; source_id ; source_type ; document_id ; location ; +NB. content ; authority ; parent_boxed ; embedding (dim 8 smoke) + +ev_mk =: 3 : 0 + NB. y already a 9-field boxed row + y +) + +EV_CORPUS =: 0 $ a: + +NB. ev_copybook_01 +EV_CORPUS =: EV_CORPUS , < ('ev_copybook_01' ; 'src_copybook_acct' ; 'copybook' ; 'ACCTREC.cpy' ; '01-ACCOUNT-BALANCE' ; '01 ACCOUNT-RECORD. 05 ACCOUNT-BALANCE PIC S9(9)V99. 05 PAYMENT-AMOUNT PIC S9(7)V99. 05 LAST-PAYMENT-DATE PIC X(8).' ; 0.95 ; (<'ev_provenance_root') ; 0.95 0.90 0.85 0.20 0.05 0 0 0) + +NB. ev_cobol_src_01 +EV_CORPUS =: EV_CORPUS , < ('ev_cobol_src_01' ; 'src_cobol_pay' ; 'cobol_source' ; 'PAYROLL.cbl' ; 'PROCEDURE DIVISION / CALC-BALANCE' ; 'COMPUTE ACCOUNT-BALANCE = ACCOUNT-BALANCE - PAYMENT-AMOUNT. IF ACCOUNT-BALANCE < ZERO THEN PERFORM OVERDRAFT-CHECK. MOVE PAYMENT-AMOUNT TO WS-LAST-PAYMENT.' ; 0.90 ; (<'ev_provenance_root') ; 0.92 0.88 0.90 0.15 0.05 0 0 0) + +NB. ev_db2_schema_01 +EV_CORPUS =: EV_CORPUS , < ('ev_db2_schema_01' ; 'src_db2_acct' ; 'db2_schema' ; 'ACCT.DDL' ; 'TABLE ACCOUNT_BALANCES' ; 'CREATE TABLE ACCOUNT_BALANCES (ACCT_ID CHAR(12), BALANCE DECIMAL(11,2), LAST_PAYMENT DECIMAL(9,2), UPDATED_TS TIMESTAMP).' ; 0.85 ; (<'ev_provenance_root') ; 0.70 0.80 0.55 0.95 0.05 0 0 0) + +NB. ev_quant_pricing_01 +EV_CORPUS =: EV_CORPUS , < ('ev_quant_pricing_01' ; 'src_quant_report' ; 'quant_report' ; 'PRICING_Q3.pdf' ; 'section:monte_carlo' ; 'Monte Carlo pricing of exotic options. Implied volatility surface and greek sensitivities. No COBOL or account balance fields.' ; 0.80 ; (<'ev_provenance_root') ; 0.05 0.05 0.05 0.05 0.95 0.90 0.85 0.10) + +NB. ev_lexical_lift_01 — weak dense, strong lexical (no dense floor) +EV_CORPUS =: EV_CORPUS , < ('ev_lexical_lift_01' ; 'src_glossary' ; 'glossary' ; 'LEGACY-GLOSSARY.txt' ; 'entry:PAYMENT-BALANCE' ; 'cobol payment balance glossary entry: PAYMENT and BALANCE fields on ACCOUNT records in legacy batch.' ; 0.70 ; (<'ev_provenance_root') ; 0.10 0.15 0.10 0.05 0.40 0.35 0.30 0.20) + +NB. ev_junk_low_auth — authority ~0.1, no parents +EV_CORPUS =: EV_CORPUS , < ('ev_junk_low_auth' ; 'src_web_scrape' ; 'web_scrape' ; 'random-blog.html' ; 'body' ; 'Someone said COBOL payment balance might work like a spreadsheet. Unverified forum post.' ; 0.10 ; (0 $ a:) ; 0.60 0.55 0.50 0.10 0.10 0 0 0.9) + +NB. --- score one row ---------------------------------------------------------- +NB. score = dense + lexical + authority - parent_penalty +NB. (no dense floor — lexical can lift weak dense) +NB. x = query ; qvec ; require_parent y = corpus row +ev_score_row =: 4 : 0 + q =. 0 ev_field x + qv =. 1 ev_field x + reqp =. 2 ev_field x + row =. y + emb =. 8 ev_field row + content =. 5 ev_field row + auth =. 6 ev_field row + parents =. boxopen 7 ev_field row + dense =. qv ev_cosine emb + lex =. q ev_lexical content + pen =. 0.0 + if. reqp *. (0 = # parents) do. pen =. 1.0 end. + dense + lex + auth - pen +) + +NB. --- retrieve --------------------------------------------------------------- +NB. y = query ; min_authority ; top_k [; require_parent] +NB. returns boxed list of (evidence_id ; score) sorted score DESC, id ASC +ev_retrieve =: 3 : 0 + args =. y + q =. 0 ev_field args + min_a =. 1 ev_field args + top_k =. 2 ev_field args + reqp =. 0 + if. 3 < # args do. reqp =. 3 ev_field args end. + NB. empty / blank query → empty result + if. 0 = # q do. 0 $ a: return. end. + if. 0 = # ev_tokens q do. 0 $ a: return. end. + qv =. ev_qembed q + ctx =. q ; qv ; reqp + ids =. 0 $ a: + scores =. 0 $ 0.0 + i =. 0 + while. i < # EV_CORPUS do. + row =. ev_unbox1 i { EV_CORPUS + auth =. 6 ev_field row + parents =. boxopen 7 ev_field row + if. auth >: min_a do. + if. (-. reqp) +. (0 < # parents) do. + sc =. ctx ev_score_row row + ids =. ids , < 0 ev_field row + scores =. scores , sc + end. + end. + i =. i + 1 + end. + if. 0 = # ids do. 0 $ a: return. end. + NB. stable: id asc then score desc → ORDER BY score DESC, evidence_id + ix =. /: ids + ix =. ix \: ix { scores + if. top_k < # ix do. ix =. top_k {. ix end. + out =. 0 $ a: + j =. 0 + while. j < # ix do. + k =. j { ix + out =. out , < ((k { ids) ; k { scores) + j =. j + 1 + end. + out +) + +NB. peel nested boxes to open string +ev_openstr =: 3 : 0 + v =. y + while. 32 = 3!:0 v do. v =. > v end. + v +) + +NB. ids only from retrieve result (single-boxed strings) +ev_ids =: 3 : 0 + rs =. y + out =. 0 $ a: + i =. 0 + while. i < # rs do. + out =. out , < ev_openstr 0 ev_field ev_unbox1 i { rs + i =. i + 1 + end. + out +) + +NB. --- smoke ------------------------------------------------------------------ +ev_smoke =: 3 : 0 + q =. 'cobol account payment balance CALC-BALANCE' + NB. 1) COBOL-ish ranks copybook/cobol/db2 above quant; quant not #1 + r1 =. ev_retrieve q ; 0.5 ; 5 ; 1 + ids1 =. ev_ids r1 + assert. 0 < # ids1 + assert. -. (< 'ev_quant_pricing_01') -: 0 { ids1 + legacy =. 'ev_copybook_01' ; 'ev_cobol_src_01' ; 'ev_db2_schema_01' + assert. 0 < # (ids1 -. ids1 -. legacy) + top3 =. 3 <. # ids1 + top3 =. top3 {. ids1 + assert. -. (< 'ev_quant_pricing_01') e. top3 + NB. 2) min_authority 0.5 drops junk + r2 =. ev_retrieve q ; 0.5 ; 10 ; 0 + ids2 =. ev_ids r2 + assert. -. (< 'ev_junk_low_auth') e. ids2 + r2open =. ev_retrieve q ; 0.0 ; 10 ; 0 + ids2o =. ev_ids r2open + assert. (< 'ev_junk_low_auth') e. ids2o + NB. 3) empty query → empty result + r3 =. ev_retrieve '' ; 0.0 ; 10 + assert. 0 = # r3 + 'ok' ; ids1 +) diff --git a/j/execution.ijs b/j/execution.ijs new file mode 100644 index 0000000000000000000000000000000000000000..dc29486d8c9093f0b4ff3c490d1f039caa9ce6cb --- /dev/null +++ b/j/execution.ijs @@ -0,0 +1,203 @@ + +NB. ============================================================================= +NB. Synthetic David — WorkItem / AgentResult sparse execution (PURE J) +NB. Locale: david. Selected agents only — no swarm fan-out. +NB. Product path: Prolog selected-set → dispatch_selected; EvidenceBundle only. +NB. Python ../execution stays smoke harness only. +NB. ============================================================================= + +cocurrent 'david' + +NB. Evidence row (boxed fields): +NB. 0 evidence_id 1 source_id 2 source_type 3 content_hash 4 authority +NB. +NB. WorkItem (boxed fields): +NB. 0 request_id 1 work_id 2 agent_id 3 task +NB. 4 evidence_rows 5 required_outputs 6 permissions 7 deadline 8 risk_policy +NB. +NB. AgentResult (boxed fields): +NB. 0 agent_id 1 status 2 conclusions 3 evidence_refs 4 provenance_root + +openscalar =: 3 : 0 + v =. y + while. (32 = 3!:0 v) *. (0 = # $ v) do. v =. > v end. + v +) + +field =: 4 : 'openscalar x { y' + +NB. Non-empty character / open content +nonempty =: 3 : 0 + v =. openscalar y + if. 32 = 3!:0 v do. 0 return. end. + 0 < # v +) + +NB. One evidence row is ok iff evidence_id, source_id, content_hash are non-empty. +NB. Bare text alone is never enough. +evidence_row_ok =: 3 : 0 + r =. y + if. 3 > # r do. 0 return. end. + (nonempty 0 { r) *. (nonempty 1 { r) *. (nonempty 3 { r) +) + +NB. Bundle = list of boxed evidence rows; empty bundle is not ok for dispatch. +bundle_ok =: 3 : 0 + rows =. y + if. 0 = # rows do. 0 return. end. + i =. 0 + ok =. 1 + while. i < # rows do. + ok =. ok *. evidence_row_ok openscalar i { rows + i =. i + 1 + end. + ok +) + +uniqseq =: 3 : 0 + if. 0 = # y do. 0 $ a: return. end. + ys =. boxopen y + out =. 0 $ a: + i =. 0 + while. i < # ys do. + v =. openscalar i { ys + if. -. (< v) e. out do. out =. out , < v end. + i =. i + 1 + end. + out +) + +fields =: 4 : 0 + rows =. y + i =. 0 + out =. 0 $ a: + while. i < # rows do. + row =. openscalar i { rows + out =. out , < x field row + i =. i + 1 + end. + out +) + +NB. Load selected agent ids from a text file (one id per line). +NB. Product handoff from Prolog / Spec J→swipl writer. +load_selected =: 3 : 0 + if. 0 = # (1!:0 < y) do. 0 $ a: return. end. + raw =. 1!:1 < y + lines =. <;._2 raw , LF + out =. 0 $ a: + i =. 0 + while. i < # lines do. + s =. openscalar i { lines + NB. drop CR if present (Windows handoff) + if. (2 = 3!:0 s) *. (0 < # s) do. + if. 13 = {: s do. s =. }: s end. + end. + if. (2 = 3!:0 s) *. (0 < # s) do. out =. out , < s end. + i =. i + 1 + end. + uniqseq out +) + +NB. Evidence ids from validated bundle rows +bundle_ids =: 3 : 0 + rows =. y + out =. 0 $ a: + i =. 0 + while. i < # rows do. + r =. openscalar i { rows + out =. out , < 0 field r + i =. i + 1 + end. + out +) + +NB. x = request: request_id ; task ; evidence_bundle ; required_outputs ; permissions ; risk_policy ; deadline +NB. y = selected agent_id list (from Prolog) +NB. Fails assert if bundle is not EvidenceBundle-ok (no bare text). +build_work_items =: 4 : 0 + bundle =. 2 field x + NB. field peels the outer box; bundle is a list of evidence rows + if. (32 = 3!:0 bundle) *. (0 = # $ bundle) do. bundle =. , < openscalar bundle end. + assert. bundle_ok bundle + aids =. uniqseq y + rid =. 0 field x + task =. 1 field x + outs =. boxopen 3 field x + perms =. boxopen 4 field x + rpol =. 5 field x + dl =. 6 field x + wis =. 0 $ a: + i =. 0 + while. i < # aids do. + aid =. openscalar i { aids + wid =. rid , ':' , aid + wi =. ( (x { y)' + +NB. x = req: caps ; inputs ; perms ; . 1 <. (6 field y) + cp =. 7 field y + sem =. 8 field y + cap =. caps jaccard rcaps + inp =. ins jaccard ain + dep =. (0 = # boxopen deps) { 0.5 1.0 + miss =. 0 < # (boxopen perms) -. boxopen gperm + rix =. riskix rcls + rpen =. rix { RISKPEN + if. rix > riskix maxr do. rpen =. rpen + 1 end. + unnec =. -. ((cap > 0) +. (cp ~: 0)) + (+/ WPOS * sem,cap,inp,hist,dep) - +/ WNEG * rpen,miss,unnec +) + +smoke =: 3 : 0 + NB. ; boxes each item once — do not pre-box scalars + req =. ('cobol';'business_rule_extraction') ; ('source_code';'copybooks') ; ('read_source';'read_copybooks') ; 'high' + legacy =. 'LegacyCobol' ; ('cobol';'business_rule_extraction') ; (<'source_code') ; (<'read_source') ; 'high' ; ('Database';'Provenance') ; 0.5 ; 0 ; 0.92 + quant =. 'Quant' ; ('pricing';'monte_carlo') ; (<'source_code') ; (<'read_source') ; 'medium' ; (0$a:) ; 0.5 ; 0 ; 0.1 + sl =. req score_agent legacy + sq =. req score_agent quant + assert. sl > sq + 'ok' ; sl ; sq +) diff --git a/j/symbolic_call.ijs b/j/symbolic_call.ijs new file mode 100644 index 0000000000000000000000000000000000000000..1dc961cb47c127725a7469bf3dcd5c22d110e540 --- /dev/null +++ b/j/symbolic_call.ijs @@ -0,0 +1,7 @@ +NB. Thin J caller for Spec Prolog eligibility. +NB. Prefer product path: eligibility.ijs + run_eligibility_smoke.ijs +NB. This file now delegates to the product smoke (requires swipl on PATH). +NB. Spawn: /home/box/j/j9.7/bin/jconsole symbolic_call.ijs + +echo 'symbolic_call.ijs → run_eligibility_smoke.ijs (J+Prolog product path)' +(0!:0) <'run_eligibility_smoke.ijs' diff --git a/logs/agent_ann_smoke.log b/logs/agent_ann_smoke.log new file mode 100644 index 0000000000000000000000000000000000000000..2bb9bd966a82f3e63a0a9b8800356a5a8ae73460 --- /dev/null +++ b/logs/agent_ann_smoke.log @@ -0,0 +1,10 @@ +query=Explain how this COBOL payment program calculates the account balance and determ... +top_k=5 +1 LegacyCobol legacy_mainframe high sim=0.406562 +2 ReverseEngineering legacy_mainframe medium sim=0.309016 +3 MigrationValidation migration high sim=0.160817 +4 Modernization migration high sim=0.154508 +5 Response control_plane medium sim=0.000000 +legacy_in_topk=True +quant_not_preferred=True +pass=True diff --git a/logs/evidence_hybrid_smoke.log b/logs/evidence_hybrid_smoke.log new file mode 100644 index 0000000000000000000000000000000000000000..7aaf5f97b5b6e63f91413cf4f9423a70242d1347 --- /dev/null +++ b/logs/evidence_hybrid_smoke.log @@ -0,0 +1,14 @@ +query=Explain how this COBOL payment program calculates the account balance and determ... +lexical_query=payment balance account +top_k=5 min_authority=0.5 require_parent=False +rank evidence_id source_type score dense lexical authority +1 ev_cobol_src_01 cobol_source score=1.589825 dense=0.583714 lexical=0.106111 authority=0.900000 +2 ev_copybook_01 copybook score=1.323832 dense=0.359546 lexical=0.014286 authority=0.950000 +3 ev_lexical_lift_01 glossary score=1.207805 dense=0.482805 lexical=0.025000 authority=0.700000 +4 ev_db2_schema_01 db2_schema score=1.104701 dense=0.243590 lexical=0.011111 authority=0.850000 +5 ev_quant_pricing_01 quant_report score=1.030079 dense=0.230079 lexical=0.000000 authority=0.800000 +junk_exists=True +junk_dropped=True +legacy_in_topk=True +quant_not_preferred=True +pass=True diff --git a/logs/j_evidence_smoke.log b/logs/j_evidence_smoke.log new file mode 100644 index 0000000000000000000000000000000000000000..148a052f97764152fc2aa9105f826b84770fd8f5 --- /dev/null +++ b/logs/j_evidence_smoke.log @@ -0,0 +1,6 @@ +┌──┬──────────────┬───────────────┬────────────────┬──────────────────┬───────────────────┐ +│ok│ev_copybook_01│ev_cobol_src_01│ev_db2_schema_01│ev_lexical_lift_01│ev_quant_pricing_01│ +└──┴──────────────┴───────────────┴────────────────┴──────────────────┴───────────────────┘ +exit_code=0 +SQL product query: sql/002_evidence_hybrid_retrieve.sql +Python evidence/ is harness only — do not grow; J evidence.ijs is the product path. diff --git a/logs/j_execution_smoke.log b/logs/j_execution_smoke.log new file mode 100644 index 0000000000000000000000000000000000000000..6b4c86e283d3182a14c70758499031ba5a009bc2 --- /dev/null +++ b/logs/j_execution_smoke.log @@ -0,0 +1,3 @@ +┌──┬─┬─┬─┐ +│ok│6│6│3│ +└──┴─┴─┴─┘ diff --git a/logs/prolog_eligibility_smoke.log b/logs/prolog_eligibility_smoke.log new file mode 100644 index 0000000000000000000000000000000000000000..be83234b0b14170048d3a05bb7ca43bc7fe43dd2 --- /dev/null +++ b/logs/prolog_eligibility_smoke.log @@ -0,0 +1,7 @@ +┌───────────┬───────────┬──────────────────┬───────────────────┬─────────────┬────────┬─────┐ +│candidates:│LegacyCobol│ReverseEngineering│MigrationValidation│Modernization│Response│Quant│ +└───────────┴───────────┴──────────────────┴───────────────────┴─────────────┴────────┴─────┘ +┌─────────┬───────────┬────────┬──────────────────┬──────────┬────┬────────┐ +│selected:│legacyCobol│database│reverseEngineering│provenance│risk│response│ +└─────────┴───────────┴────────┴──────────────────┴──────────┴────┴────────┘ +eligibility_smoke: OK diff --git a/logs/prolog_registry_sync.log b/logs/prolog_registry_sync.log new file mode 100644 index 0000000000000000000000000000000000000000..f372e37e6c52d3bafacd72302846f908111c3f68 --- /dev/null +++ b/logs/prolog_registry_sync.log @@ -0,0 +1,3 @@ +wrote /tmp/registry_facts.rqcYnl.pl (10 agents) +sync ok: 10 agents; Quant present as negative +prolog_registry_sync: OK diff --git a/logs/prolog_selected_cobol.raw b/logs/prolog_selected_cobol.raw new file mode 100644 index 0000000000000000000000000000000000000000..12bb9ecd733d6190b092acd6b0f27c4aa0a768a8 --- /dev/null +++ b/logs/prolog_selected_cobol.raw @@ -0,0 +1,2 @@ +selected:[legacyCobol,database,reverseEngineering,provenance,risk,response] +smoke_cobol: OK diff --git a/logs/prolog_smoke_cobol.log b/logs/prolog_smoke_cobol.log new file mode 100644 index 0000000000000000000000000000000000000000..12bb9ecd733d6190b092acd6b0f27c4aa0a768a8 --- /dev/null +++ b/logs/prolog_smoke_cobol.log @@ -0,0 +1,2 @@ +selected:[legacyCobol,database,reverseEngineering,provenance,risk,response] +smoke_cobol: OK diff --git a/logs/router_smoke.log b/logs/router_smoke.log new file mode 100644 index 0000000000000000000000000000000000000000..9ef525c4b4694c975512b2442299d962db752e1f --- /dev/null +++ b/logs/router_smoke.log @@ -0,0 +1,4 @@ +[2026-10-07 06:14:51 PDT] router_smoke minimum_agent_set selected=['LegacyCobol', 'Database', 'Provenance', 'Risk', 'Response'] +[2026-10-07 06:14:51 PDT] Quant absent=True; Risk present=True; Response present=True +[2026-10-07 06:14:51 PDT] note: scores came from scorer.score_agent only (via symbolic.minimum_agent_set; no second scorer) +[2026-10-07 06:14:51 PDT] evidence_smoke query='cobol account payment balance CALC-BALANCE' top_evidence_ids=['ev_copybook_01', 'ev_cobol_src_01', 'ev_db2_schema_01', 'ev_lexical_lift_01', 'ev_quant_pricing_01'] diff --git a/logs/selected_cobol.txt b/logs/selected_cobol.txt new file mode 100644 index 0000000000000000000000000000000000000000..9b4a25c54a5b3cdb1881516bbf8e5cbe1ff6b737 --- /dev/null +++ b/logs/selected_cobol.txt @@ -0,0 +1,6 @@ +legacyCobol +database +reverseEngineering +provenance +risk +response diff --git a/registry/agent_descriptor.schema.json b/registry/agent_descriptor.schema.json new file mode 100644 index 0000000000000000000000000000000000000000..4f427eee327ff00088c3a46d776368e3c8ab25b5 --- /dev/null +++ b/registry/agent_descriptor.schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://david.local/schemas/agent_descriptor.json", + "title": "AgentDescriptor", + "type": "object", + "required": [ + "agent_id", + "domain", + "capabilities", + "languages", + "input_types", + "output_types", + "required_evidence", + "permissions", + "risk_class", + "dependencies", + "execution_endpoint" + ], + "properties": { + "agent_id": { "type": "string", "minLength": 1 }, + "domain": { "type": "string" }, + "capabilities": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true + }, + "languages": { + "type": "array", + "items": { "type": "string" } + }, + "input_types": { + "type": "array", + "items": { "type": "string" } + }, + "output_types": { + "type": "array", + "items": { "type": "string" } + }, + "required_evidence": { + "type": "array", + "items": { "type": "string" } + }, + "permissions": { + "type": "array", + "items": { "type": "string" } + }, + "risk_class": { + "type": "string", + "enum": ["low", "medium", "high", "critical"] + }, + "dependencies": { + "type": "array", + "items": { "type": "string" }, + "description": "Other agent_id values required for dependency closure" + }, + "embedding": { + "type": "array", + "items": { "type": "number" }, + "description": "Optional; usually stored only in pgvector" + }, + "execution_endpoint": { "type": "string" }, + "control_plane": { + "type": "boolean", + "default": false, + "description": "If true, always considered for Risk/Audit/Response closure" + } + }, + "additionalProperties": false +} diff --git a/registry/seed_agents.yaml b/registry/seed_agents.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4b7a0221230d39395ddd4e73840a0139a0a9e88c --- /dev/null +++ b/registry/seed_agents.yaml @@ -0,0 +1,132 @@ +# Seed routable agents (not permanently active processes) + +- agent_id: LegacyCobol + domain: legacy_mainframe + capabilities: + - cobol + - copybook_analysis + - control_flow + - data_flow + - business_rule_extraction + - modernization_analysis + languages: [cobol] + input_types: [source_code, copybooks, compiler_metadata] + output_types: [call_graph, business_rules, data_flow] + required_evidence: [source_code, copybooks, compiler_metadata] + permissions: [read_source, read_copybooks] + risk_class: high + dependencies: [Database, ReverseEngineering, Provenance] + execution_endpoint: agent://LegacyCobol + control_plane: false + +- agent_id: Database + domain: data + capabilities: [schema_analysis, db2, sql] + languages: [sql] + input_types: [db_schema, queries] + output_types: [schema_map, data_lineage] + required_evidence: [db_schema] + permissions: [read_schema] + risk_class: medium + dependencies: [] + execution_endpoint: agent://Database + +- agent_id: ReverseEngineering + domain: legacy_mainframe + capabilities: [call_graph, control_flow, data_flow] + languages: [cobol, java] + input_types: [source_code] + output_types: [call_graph, cfg] + required_evidence: [source_code] + permissions: [read_source] + risk_class: medium + dependencies: [] + execution_endpoint: agent://ReverseEngineering + +- agent_id: Modernization + domain: migration + capabilities: [cobol_to_java, api_mapping] + languages: [cobol, java] + input_types: [business_rules, call_graph] + output_types: [migration_plan, java_skeleton] + required_evidence: [business_rules] + permissions: [read_source, write_plan] + risk_class: high + dependencies: [LegacyCobol, Provenance] + execution_endpoint: agent://Modernization + +- agent_id: MigrationValidation + domain: migration + capabilities: [equivalence, behavioral_compare] + languages: [cobol, java] + input_types: [legacy_output, java_output] + output_types: [equivalence_result] + required_evidence: [legacy_output, java_output] + permissions: [read_outputs] + risk_class: high + dependencies: [Provenance, LegacyCobol] + execution_endpoint: agent://MigrationValidation + +- agent_id: Provenance + domain: evidence + capabilities: [provenance_dag, content_hash, authority] + languages: [] + input_types: [evidence, facts] + output_types: [provenance_root, evidence_graph] + required_evidence: [] + permissions: [read_evidence, write_provenance] + risk_class: medium + dependencies: [] + execution_endpoint: agent://Provenance + +- agent_id: Risk + domain: control_plane + capabilities: [risk_policy, escalation] + languages: [] + input_types: [agent_results, risk_signals] + output_types: [risk_decision] + required_evidence: [] + permissions: [read_all_results] + risk_class: critical + dependencies: [] + execution_endpoint: agent://Risk + control_plane: true + +- agent_id: Audit + domain: control_plane + capabilities: [audit_trail, compliance] + languages: [] + input_types: [provenance_root, agent_results] + output_types: [audit_record] + required_evidence: [] + permissions: [read_all_results] + risk_class: high + dependencies: [Provenance] + execution_endpoint: agent://Audit + control_plane: true + +- agent_id: Response + domain: control_plane + capabilities: [synthesis, user_response] + languages: [] + input_types: [conclusions, evidence_bundle] + output_types: [response] + required_evidence: [] + permissions: [read_conclusions] + risk_class: medium + dependencies: [] + execution_endpoint: agent://Response + control_plane: true + +- agent_id: Quant + domain: markets + capabilities: [pricing, monte_carlo, greeks] + languages: [python] + input_types: [market_data, positions] + output_types: [price, risk_metrics] + required_evidence: [market_data] + permissions: [read_market_data] + risk_class: medium + dependencies: [] + execution_endpoint: agent://Quant + control_plane: false diff --git a/router/registry_facts.pl b/router/registry_facts.pl new file mode 100644 index 0000000000000000000000000000000000000000..722f083297502d4ce80f5996d61071e523e2418c --- /dev/null +++ b/router/registry_facts.pl @@ -0,0 +1,98 @@ +% SPDX-License-Identifier: AGPL-3.0-only +% Copyright (C) 2026 David contributors +% GENERATED by scripts/gen_registry_facts.py from registry/seed_agents.yaml +% Do not edit by hand — regenerate via scripts/check_prolog_registry_sync.sh +% +% Agent atom map: registry agent_id → Prolog atom = downcase first letter +% LegacyCobol → legacyCobol, Risk → risk, Quant → quant +% Predicates: agent_id/2, agent/1, control_plane/1, capability/2, +% permission/2, risk_class/2, depends/2 + +agent_id('LegacyCobol', legacyCobol). +agent_id('Database', database). +agent_id('ReverseEngineering', reverseEngineering). +agent_id('Modernization', modernization). +agent_id('MigrationValidation', migrationValidation). +agent_id('Provenance', provenance). +agent_id('Risk', risk). +agent_id('Audit', audit). +agent_id('Response', response). +agent_id('Quant', quant). + +agent(legacyCobol). +agent(database). +agent(reverseEngineering). +agent(modernization). +agent(migrationValidation). +agent(provenance). +agent(risk). +agent(audit). +agent(response). +agent(quant). + +control_plane(risk). +control_plane(audit). +control_plane(response). + +capability(legacyCobol, cobol). +capability(legacyCobol, copybook_analysis). +capability(legacyCobol, control_flow). +capability(legacyCobol, data_flow). +capability(legacyCobol, business_rule_extraction). +capability(legacyCobol, modernization_analysis). +capability(database, schema_analysis). +capability(database, db2). +capability(database, sql). +capability(reverseEngineering, call_graph). +capability(reverseEngineering, control_flow). +capability(reverseEngineering, data_flow). +capability(modernization, cobol_to_java). +capability(modernization, api_mapping). +capability(migrationValidation, equivalence). +capability(migrationValidation, behavioral_compare). +capability(provenance, provenance_dag). +capability(provenance, content_hash). +capability(provenance, authority). +capability(risk, risk_policy). +capability(risk, escalation). +capability(audit, audit_trail). +capability(audit, compliance). +capability(response, synthesis). +capability(response, user_response). +capability(quant, pricing). +capability(quant, monte_carlo). +capability(quant, greeks). + +permission(legacyCobol, read_source). +permission(legacyCobol, read_copybooks). +permission(database, read_schema). +permission(reverseEngineering, read_source). +permission(modernization, read_source). +permission(modernization, write_plan). +permission(migrationValidation, read_outputs). +permission(provenance, read_evidence). +permission(provenance, write_provenance). +permission(risk, read_all_results). +permission(audit, read_all_results). +permission(response, read_conclusions). +permission(quant, read_market_data). + +risk_class(legacyCobol, high). +risk_class(database, medium). +risk_class(reverseEngineering, medium). +risk_class(modernization, high). +risk_class(migrationValidation, high). +risk_class(provenance, medium). +risk_class(risk, critical). +risk_class(audit, high). +risk_class(response, medium). +risk_class(quant, medium). + +depends(legacyCobol, database). +depends(legacyCobol, reverseEngineering). +depends(legacyCobol, provenance). +depends(modernization, legacyCobol). +depends(modernization, provenance). +depends(migrationValidation, provenance). +depends(migrationValidation, legacyCobol). +depends(audit, provenance). diff --git a/router/scorer.py b/router/scorer.py new file mode 100644 index 0000000000000000000000000000000000000000..db979de1f41f36a7fc4d1e5b513b368bd2ef6cb9 --- /dev/null +++ b/router/scorer.py @@ -0,0 +1,147 @@ +"""Deterministic sparse-MoA agent scoring (no LLM in the selection loop).""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Iterable + + +WEIGHTS = { + "semantic_similarity": 1.0, + "capability_match": 1.2, + "input_match": 1.0, + "historical_success": 0.8, + "dependency_coverage": 0.6, + "risk_penalty": 0.9, + "permission_penalty": 1.5, + "unnecessary_agent_penalty": 0.7, +} + +RISK_PENALTY = {"low": 0.0, "medium": 0.15, "high": 0.35, "critical": 0.55} + + +@dataclass(frozen=True) +class AgentDescriptor: + agent_id: str + domain: str + capabilities: frozenset[str] + input_types: frozenset[str] + permissions: frozenset[str] + risk_class: str + dependencies: frozenset[str] + historical_success: float = 0.0 + control_plane: bool = False + + +@dataclass(frozen=True) +class RequestContext: + required_capabilities: frozenset[str] + available_inputs: frozenset[str] + granted_permissions: frozenset[str] + max_risk_class: str = "high" + semantic_scores: dict[str, float] = field(default_factory=dict) + + +_RISK_ORDER = ("low", "medium", "high", "critical") + + +def _jaccard(a: frozenset[str], b: frozenset[str]) -> float: + if not a and not b: + return 1.0 + if not a or not b: + return 0.0 + return len(a & b) / len(a | b) + + +def risk_exceeds(agent_risk: str, max_risk: str) -> bool: + """True if agent_risk is strictly above max_risk (low < medium < high < critical).""" + return _RISK_ORDER.index(agent_risk) > _RISK_ORDER.index(max_risk) + + +def _risk_exceeds(agent_risk: str, max_risk: str) -> bool: + return risk_exceeds(agent_risk, max_risk) + + +def score_agent(agent: AgentDescriptor, req: RequestContext) -> float: + """score = Σ positive terms − penalties. Higher is better.""" + sem = req.semantic_scores.get(agent.agent_id, 0.0) + cap = _jaccard(agent.capabilities, req.required_capabilities) + inp = _jaccard(agent.input_types, req.available_inputs) + hist = max(0.0, min(1.0, agent.historical_success)) + # Dependency coverage: fraction of deps present in semantic top pool (caller may refine). + dep_cov = 1.0 if not agent.dependencies else 0.5 + + missing_perms = agent.permissions - req.granted_permissions + perm_pen = 1.0 if missing_perms else 0.0 + risk_pen = RISK_PENALTY.get(agent.risk_class, 0.5) + if _risk_exceeds(agent.risk_class, req.max_risk_class): + risk_pen += 1.0 + + # Mild penalty if the agent shares no required capabilities (unnecessary). + unnecessary = 0.0 if cap > 0.0 or agent.control_plane else 1.0 + + return ( + WEIGHTS["semantic_similarity"] * sem + + WEIGHTS["capability_match"] * cap + + WEIGHTS["input_match"] * inp + + WEIGHTS["historical_success"] * hist + + WEIGHTS["dependency_coverage"] * dep_cov + - WEIGHTS["risk_penalty"] * risk_pen + - WEIGHTS["permission_penalty"] * perm_pen + - WEIGHTS["unnecessary_agent_penalty"] * unnecessary + ) + + +def dependency_closure( + selected: Iterable[str], + registry: dict[str, AgentDescriptor], +) -> list[str]: + """Expand selected agents with required dependencies (deterministic BFS).""" + out: list[str] = [] + seen: set[str] = set() + queue = list(selected) + while queue: + aid = queue.pop(0) + if aid in seen: + continue + seen.add(aid) + out.append(aid) + agent = registry.get(aid) + if agent is None: + continue + for dep in sorted(agent.dependencies): + if dep not in seen: + queue.append(dep) + return out + + +def select_minimum_set( + candidates: list[AgentDescriptor], + req: RequestContext, + registry: dict[str, AgentDescriptor], + top_k: int = 20, + include_control_plane: bool = True, +) -> list[str]: + """ + Pipeline: score → sort deterministically → top-K → dependency closure + → optional control-plane agents → return agent_ids. + """ + ranked = sorted( + candidates, + key=lambda a: (-score_agent(a, req), a.agent_id), + )[:top_k] + + # Keep agents that match at least one required capability (or control plane later). + core = [ + a.agent_id + for a in ranked + if a.capabilities & req.required_capabilities + ] + closed = dependency_closure(core, registry) + + if include_control_plane: + for a in candidates: + if a.control_plane and a.agent_id not in closed: + closed.append(a.agent_id) + + return closed diff --git a/router/symbolic.py b/router/symbolic.py new file mode 100644 index 0000000000000000000000000000000000000000..b1c2ee0f8d83bf8307ba1dd086bb349bdbf2b4f1 --- /dev/null +++ b/router/symbolic.py @@ -0,0 +1,153 @@ +"""Symbolic hard filters and minimum-agent constraint set (Spec / Hilbert). + +Hard filters are predicates (True/False), not soft score penalties. +Scorer remains source of truth for scores — import score_agent / dependency_closure; +never redefine WEIGHTS or score_agent. +""" + +from __future__ import annotations + +from typing import Iterable, Sequence + +from scorer import ( + AgentDescriptor, + RequestContext, + dependency_closure, + risk_exceeds, + score_agent, +) + + +def capability_eligible(agent: AgentDescriptor, req: RequestContext) -> bool: + """Agent shares at least one required capability, or is control-plane.""" + if agent.control_plane: + return True + return bool(agent.capabilities & req.required_capabilities) + + +def permission_eligible(agent: AgentDescriptor, req: RequestContext) -> bool: + """Agent permissions must be a subset of granted permissions.""" + return agent.permissions <= req.granted_permissions + + +def risk_eligible(agent: AgentDescriptor, req: RequestContext) -> bool: + """Agent risk_class must not exceed req.max_risk_class.""" + return not risk_exceeds(agent.risk_class, req.max_risk_class) + + +def apply_symbolic_filters( + candidates: Iterable[AgentDescriptor], + req: RequestContext, +) -> list[AgentDescriptor]: + """Return agents that pass capability, permission, and risk hard filters.""" + return [ + a + for a in candidates + if capability_eligible(a, req) + and permission_eligible(a, req) + and risk_eligible(a, req) + ] + + +def _greedy_cover( + eligible: Sequence[AgentDescriptor], + req: RequestContext, +) -> list[str]: + """Greedy set-cover over required_capabilities (deterministic).""" + uncovered = set(req.required_capabilities) + remaining = list(eligible) + picked: list[str] = [] + scores = {a.agent_id: score_agent(a, req) for a in remaining} + + while uncovered and remaining: + # Primary: most still-uncovered caps; secondary: highest score; tertiary: agent_id + def key(a: AgentDescriptor) -> tuple: + cover = len(a.capabilities & uncovered) + return (-cover, -scores[a.agent_id], a.agent_id) + + best = min(remaining, key=key) + cover = len(best.capabilities & uncovered) + if cover == 0: + break + picked.append(best.agent_id) + uncovered -= best.capabilities + remaining = [a for a in remaining if a.agent_id != best.agent_id] + + return picked + + +def _closure_passes_hard_filters( + agent_id: str, + req: RequestContext, + registry: dict[str, AgentDescriptor], +) -> bool: + """True iff agent_id and all transitive deps pass permission + risk. + + capability_eligible is waived for pure dependency agents pulled in solely + as deps (they need not intersect required_capabilities). + """ + for cid in dependency_closure([agent_id], registry): + agent = registry.get(cid) + if agent is None: + return False + if not permission_eligible(agent, req): + return False + if not risk_eligible(agent, req): + return False + return True + + +def minimum_agent_set( + candidates: list[AgentDescriptor], + req: RequestContext, + registry: dict[str, AgentDescriptor], + top_k: int = 20, + mandatory_control_plane: Sequence[str] = ("Risk", "Response"), +) -> list[str]: + """ + Hard-filter + greedy cover + dependency closure + mandatory control plane. + + Order: greedy picks, then closure BFS order, then mandatory control plane + in the given order. Unique and stable. + + After closure, every agent must pass permission_eligible and risk_eligible. + capability_eligible is waived for pure dependency agents. If a dependency + fails those hard filters, its parent is ineligible — that parent and its + incomplete closure are dropped. + + Mandatory control-plane agents skip the risk filter (matching scorer + include_control_plane) but still require permission_eligible. + """ + # 1–2. Score, sort deterministically, take top_k + ranked = sorted( + candidates, + key=lambda a: (-score_agent(a, req), a.agent_id), + )[:top_k] + + # 3. Hard filters on the pool + eligible = apply_symbolic_filters(ranked, req) + + # 4. Greedy set-cover + greedy_ids = _greedy_cover(eligible, req) + + # 5–6. Closure per pick; drop parents whose closure fails perm/risk + kept: list[str] = [] + for aid in greedy_ids: + if _closure_passes_hard_filters(aid, req, registry): + kept.append(aid) + + # Closure BFS over surviving greedy picks (full registry) + result = dependency_closure(kept, registry) + + # 7. Mandatory control plane: permission only (skip risk), like include_control_plane + for mid in mandatory_control_plane: + if mid in result: + continue + agent = registry.get(mid) + if agent is None: + continue + if permission_eligible(agent, req): + result.append(mid) + + # 8. Already unique via dependency_closure + append checks + return result diff --git a/router/symbolic_rules.pl b/router/symbolic_rules.pl new file mode 100644 index 0000000000000000000000000000000000000000..cc95d036e11464dfbb71585cbc1e8093f5144095 --- /dev/null +++ b/router/symbolic_rules.pl @@ -0,0 +1,203 @@ +% SPDX-License-Identifier: AGPL-3.0-only +% Copyright (C) 2026 David contributors +% Executable eligibility rules for Synthetic David sparse router. +% Authoritative symbolic layer: embeddings propose; these rules decide. +% Agent facts live in registry_facts.pl (generated from seed_agents.yaml). +% Atom map: registry agent_id → Prolog atom via downcase-first-letter +% (see agent_id/2 in registry_facts.pl). Quant is a negative agent. +% Run: swipl -q -s symbolic_rules.pl -g smoke_cobol -t halt +% Product path: J writes request + ann_candidate facts, calls run_eligibility/1. + +:- style_check(-singleton). +:- include('registry_facts.pl'). + +% ---- risk order ---- +risk_rank(low, 0). +risk_rank(medium, 1). +risk_rank(high, 2). +risk_rank(critical, 3). + +risk_exceeds(AgentRisk, MaxRisk) :- + risk_rank(AgentRisk, A), + risk_rank(MaxRisk, M), + A > M. + +% ---- request context (dynamic; J / smoke asserts) ---- +% required_capability(Req, Cap). +% granted_permission(Req, Perm). +% max_risk_class(Req, Class). +% ann_candidate(Req, AgentAtom). % optional; when present, core pick restricted + +:- dynamic required_capability/2, granted_permission/2, max_risk_class/2. +:- dynamic ann_candidate/2. + +% ---- hard filters ---- +capability_eligible(Agent, Req) :- + control_plane(Agent), + agent(Agent). +capability_eligible(Agent, Req) :- + agent(Agent), + capability(Agent, Cap), + required_capability(Req, Cap). + +permission_eligible(Agent, Req) :- + agent(Agent), + forall(permission(Agent, P), granted_permission(Req, P)). + +risk_eligible(Agent, Req) :- + agent(Agent), + risk_class(Agent, AR), + max_risk_class(Req, MR), + \+ risk_exceeds(AR, MR). + +% Core eligible: all three hard filters (control-plane still needs risk unless mandatory). +eligible(Agent, Req) :- + capability_eligible(Agent, Req), + permission_eligible(Agent, Req), + risk_eligible(Agent, Req). + +% Dependency agents may skip capability intersection; still need permission + risk. +dep_eligible(Agent, Req) :- + agent(Agent), + permission_eligible(Agent, Req), + risk_eligible(Agent, Req). + +% Mandatory control plane: permission only (matches scaffold; Risk may be critical). +mandatory_control_eligible(Agent, Req) :- + control_plane(Agent), + permission_eligible(Agent, Req). + +% ---- dependency closure (BFS via setof/findall) ---- +closure(Seed, Closed) :- + closure_(Seed, [], Closed). + +closure_([], Acc, Closed) :- + reverse(Acc, Closed). +closure_([H|T], Acc, Closed) :- + memberchk(H, Acc), !, + closure_(T, Acc, Closed). +closure_([H|T], Acc, Closed) :- + findall(D, depends(H, D), Deps), + append(T, Deps, Queue), + closure_(Queue, [H|Acc], Closed). + +% ---- minimum agent set ---- +% 1. Collect agents that cover at least one required capability and pass hard filters +% (if ann_candidate/2 facts exist for Req, restrict core seeds to those candidates) +% 2. Greedy cover of required capabilities (order by agent atom for determinism) +% 3. Dependency closure; drop incomplete parents if a dep fails dep_eligible +% 4. Append mandatory Risk + Response if permitted +% Quant has no overlapping caps for COBOL requests → dropped. + +covers_required(Agent, Req, Cap) :- + required_capability(Req, Cap), + capability(Agent, Cap). + +% When ANN candidates are asserted for Req, only those may be core seeds. +% Deps and mandatory control-plane agents are still closed in below. +in_ann_or_unrestricted(Agent, Req) :- + ( ann_candidate(Req, _) -> ann_candidate(Req, Agent) ; true ). + +candidate_core(Agent, Req) :- + eligible(Agent, Req), + \+ control_plane(Agent), + covers_required(Agent, Req, _), + in_ann_or_unrestricted(Agent, Req). + +greedy_cover(Req, Selected) :- + findall(Cap, required_capability(Req, Cap), Caps0), + sort(Caps0, Caps), + findall(A, candidate_core(A, Req), Cands0), + sort(Cands0, Cands), + greedy_cover_(Caps, Cands, Req, [], Selected). + +greedy_cover_([], _, _, Acc, Selected) :- + reverse(Acc, Selected). +greedy_cover_(Uncovered, Cands, Req, Acc, Selected) :- + Uncovered \= [], + best_cover(Uncovered, Cands, Acc, Best, CoverN), + CoverN > 0, !, + findall(C, (member(C, Uncovered), \+ capability(Best, C)), Rest), + greedy_cover_(Rest, Cands, Req, [Best|Acc], Selected). +greedy_cover_(_, _, _, Acc, Selected) :- + reverse(Acc, Selected). + +best_cover(Uncovered, Cands, Already, Best, CoverN) :- + findall(N-A, ( + member(A, Cands), + \+ memberchk(A, Already), + findall(C, (member(C, Uncovered), capability(A, C)), Cs), + length(Cs, N), + N > 0 + ), Pairs0), + sort(0, @>=, Pairs0, Sorted), + Sorted = [CoverN-Best|_]. + +filter_closure([], _, []). +filter_closure([A|As], Req, [A|Bs]) :- + ( memberchk(A, As) -> fail ; true ), + ( candidate_core(A, Req) -> true + ; depends_from_selected(A, [A|As]) -> dep_eligible(A, Req) + ; dep_eligible(A, Req) + ), !, + filter_closure(As, Req, Bs). +filter_closure([_|As], Req, Bs) :- + filter_closure(As, Req, Bs). + +closed_ok(Closed, Req, Seed) :- + forall(member(A, Seed), candidate_core(A, Req)), + forall(member(A, Closed), dep_eligible(A, Req)). + +minimum_agent_set(Req, Final) :- + greedy_cover(Req, Seed), + closure(Seed, Closed0), + include(dep_ok(Req), Closed0, Closed), + include(deps_satisfied(Closed), Closed, CoreKept), + mandatory_control(Req, Mand), + append(CoreKept, Mand, Raw), + list_to_set(Raw, Final). + +dep_ok(Req, Agent) :- dep_eligible(Agent, Req). + +deps_satisfied(Closed, Agent) :- + forall(depends(Agent, D), memberchk(D, Closed)). + +mandatory_control(Req, Mand) :- + findall(A, ( + member(A, [risk, response]), + mandatory_control_eligible(A, Req) + ), Mand). + +% ---- product entry: print selected atoms (J parses this line) ---- +run_eligibility(Req) :- + minimum_agent_set(Req, Selected), + format('selected:~w~n', [Selected]). + +% ---- COBOL smoke request (no ANN candidates → full registry core) ---- +assert_cobol_req :- + retractall(required_capability(cobol_req, _)), + retractall(granted_permission(cobol_req, _)), + retractall(max_risk_class(cobol_req, _)), + retractall(ann_candidate(cobol_req, _)), + assertz(required_capability(cobol_req, cobol)), + assertz(required_capability(cobol_req, business_rule_extraction)), + assertz(granted_permission(cobol_req, read_source)), + assertz(granted_permission(cobol_req, read_copybooks)), + assertz(granted_permission(cobol_req, read_schema)), + assertz(granted_permission(cobol_req, read_evidence)), + assertz(granted_permission(cobol_req, write_provenance)), + assertz(granted_permission(cobol_req, read_all_results)), + assertz(granted_permission(cobol_req, read_conclusions)), + assertz(max_risk_class(cobol_req, high)). + +smoke_cobol :- + assert_cobol_req, + minimum_agent_set(cobol_req, Selected), + format('selected:~w~n', [Selected]), + ( memberchk(legacyCobol, Selected) -> true ; format('FAIL missing LegacyCobol~n'), halt(1) ), + ( memberchk(database, Selected) -> true ; format('FAIL missing Database~n'), halt(1) ), + ( memberchk(provenance, Selected) -> true ; format('FAIL missing Provenance~n'), halt(1) ), + ( memberchk(quant, Selected) -> format('FAIL Quant present~n'), halt(1) ; true ), + ( memberchk(risk, Selected) -> true ; format('FAIL missing Risk~n'), halt(1) ), + ( memberchk(response, Selected) -> true ; format('FAIL missing Response~n'), halt(1) ), + format('smoke_cobol: OK~n'). diff --git a/router/test_scorer.py b/router/test_scorer.py new file mode 100644 index 0000000000000000000000000000000000000000..16238fbe81363c002185dfddd8c63b82710b7d0b --- /dev/null +++ b/router/test_scorer.py @@ -0,0 +1,71 @@ +"""Smoke tests for deterministic sparse selection.""" + +from __future__ import annotations + +import unittest + +from scorer import AgentDescriptor, RequestContext, score_agent, select_minimum_set + + +def _agent(aid, caps, deps=(), control=False, risk="medium", perms=(), inputs=()): + return AgentDescriptor( + agent_id=aid, + domain="test", + capabilities=frozenset(caps), + input_types=frozenset(inputs or ("source_code",)), + permissions=frozenset(perms or ("read_source",)), + risk_class=risk, + dependencies=frozenset(deps), + historical_success=0.5, + control_plane=control, + ) + + +class ScorerTests(unittest.TestCase): + def setUp(self): + self.registry = { + "LegacyCobol": _agent( + "LegacyCobol", + ["cobol", "business_rule_extraction"], + deps=("Database", "Provenance"), + risk="high", + ), + "Database": _agent("Database", ["schema_analysis"], inputs=("db_schema",)), + "Provenance": _agent("Provenance", ["provenance_dag"]), + "Quant": _agent("Quant", ["pricing", "monte_carlo"]), + "Risk": _agent("Risk", ["risk_policy"], control=True, risk="critical"), + "Response": _agent("Response", ["synthesis"], control=True), + } + self.candidates = list(self.registry.values()) + self.req = RequestContext( + required_capabilities=frozenset({"cobol", "business_rule_extraction"}), + available_inputs=frozenset({"source_code", "copybooks"}), + granted_permissions=frozenset({"read_source", "read_copybooks"}), + max_risk_class="high", + semantic_scores={ + "LegacyCobol": 0.92, + "Database": 0.4, + "Quant": 0.1, + "Provenance": 0.3, + "Risk": 0.2, + "Response": 0.2, + }, + ) + + def test_cobol_query_excludes_quant(self): + selected = select_minimum_set(self.candidates, self.req, self.registry) + self.assertIn("LegacyCobol", selected) + self.assertIn("Database", selected) + self.assertIn("Provenance", selected) + self.assertNotIn("Quant", selected) + self.assertIn("Risk", selected) + self.assertIn("Response", selected) + + def test_score_orders_legacy_above_quant(self): + legacy = score_agent(self.registry["LegacyCobol"], self.req) + quant = score_agent(self.registry["Quant"], self.req) + self.assertGreater(legacy, quant) + + +if __name__ == "__main__": + unittest.main() diff --git a/router/test_symbolic.py b/router/test_symbolic.py new file mode 100644 index 0000000000000000000000000000000000000000..03ba649fee8adacf89241de7db597b997ee00d2e --- /dev/null +++ b/router/test_symbolic.py @@ -0,0 +1,124 @@ +"""Smoke tests for symbolic hard filters + minimum_agent_set.""" + +from __future__ import annotations + +import unittest + +from scorer import AgentDescriptor, RequestContext +from symbolic import ( + apply_symbolic_filters, + capability_eligible, + minimum_agent_set, + permission_eligible, + risk_eligible, +) + + +def _agent(aid, caps, deps=(), control=False, risk="medium", perms=(), inputs=()): + return AgentDescriptor( + agent_id=aid, + domain="test", + capabilities=frozenset(caps), + input_types=frozenset(inputs or ("source_code",)), + permissions=frozenset(perms or ("read_source",)), + risk_class=risk, + dependencies=frozenset(deps), + historical_success=0.5, + control_plane=control, + ) + + +class SymbolicTests(unittest.TestCase): + def setUp(self): + self.registry = { + "LegacyCobol": _agent( + "LegacyCobol", + ["cobol", "business_rule_extraction"], + deps=("Database", "Provenance"), + risk="high", + ), + "Database": _agent("Database", ["schema_analysis"], inputs=("db_schema",)), + "Provenance": _agent("Provenance", ["provenance_dag"]), + "Quant": _agent("Quant", ["pricing", "monte_carlo"]), + "Risk": _agent("Risk", ["risk_policy"], control=True, risk="critical"), + "Response": _agent("Response", ["synthesis"], control=True), + } + self.candidates = list(self.registry.values()) + self.req = RequestContext( + required_capabilities=frozenset({"cobol", "business_rule_extraction"}), + available_inputs=frozenset({"source_code", "copybooks"}), + granted_permissions=frozenset({"read_source", "read_copybooks"}), + max_risk_class="high", + semantic_scores={ + "LegacyCobol": 0.92, + "Database": 0.4, + "Quant": 0.1, + "Provenance": 0.3, + "Risk": 0.2, + "Response": 0.2, + }, + ) + + def test_cobol_query_minimum_set(self): + selected = minimum_agent_set(self.candidates, self.req, self.registry) + self.assertIn("LegacyCobol", selected) + self.assertIn("Database", selected) + self.assertIn("Provenance", selected) + self.assertNotIn("Quant", selected) + self.assertIn("Risk", selected) + self.assertIn("Response", selected) + + def test_quant_never_selected_for_cobol(self): + selected = minimum_agent_set(self.candidates, self.req, self.registry) + self.assertNotIn("Quant", selected) + self.assertFalse(capability_eligible(self.registry["Quant"], self.req)) + + def test_permission_filter_drops_ungranted(self): + sneaky = _agent( + "SneakyWrite", + ["cobol"], + perms=("read_source", "write_prod"), + ) + req = RequestContext( + required_capabilities=frozenset({"cobol"}), + available_inputs=frozenset({"source_code"}), + granted_permissions=frozenset({"read_source"}), + max_risk_class="high", + semantic_scores={"SneakyWrite": 0.99}, + ) + self.assertFalse(permission_eligible(sneaky, req)) + filtered = apply_symbolic_filters([sneaky], req) + self.assertEqual(filtered, []) + selected = minimum_agent_set([sneaky], req, {"SneakyWrite": sneaky}) + self.assertNotIn("SneakyWrite", selected) + + def test_risk_filter_drops_critical_when_max_medium(self): + critical = _agent("HotPath", ["cobol"], risk="critical") + req = RequestContext( + required_capabilities=frozenset({"cobol"}), + available_inputs=frozenset({"source_code"}), + granted_permissions=frozenset({"read_source"}), + max_risk_class="medium", + semantic_scores={"HotPath": 0.99, "Risk": 0.2, "Response": 0.2}, + ) + self.assertFalse(risk_eligible(critical, req)) + filtered = apply_symbolic_filters([critical], req) + self.assertEqual(filtered, []) + + # Mandatory control plane still included despite Risk being critical + # (risk gate skipped for mandatory CP; permission still required). + registry = { + "HotPath": critical, + "Risk": self.registry["Risk"], + "Response": self.registry["Response"], + } + selected = minimum_agent_set( + list(registry.values()), req, registry + ) + self.assertNotIn("HotPath", selected) + self.assertIn("Risk", selected) + self.assertIn("Response", selected) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/README_agent_ann.md b/scripts/README_agent_ann.md new file mode 100644 index 0000000000000000000000000000000000000000..a9d2969182588d188b7bfba25407107ce16a7345 --- /dev/null +++ b/scripts/README_agent_ann.md @@ -0,0 +1,11 @@ +# Live AGENT-index Postgres smoke + +Product indexes: `sql/001_agent_and_evidence_indexes.sql` (pgvector). + +Harness (Python, not product): +- `load_agent_registry.py` — `registry/seed_agents.yaml` → `agent_registry`, deterministic hashed bag-of-tokens → `vector(1536)` +- `agent_ann_smoke.py` — COBOL balance query ANN top-K (`<=>` cosine) + +Done criteria: LegacyCobol in top-K; Quant not preferred before symbolic filter. + +Measured log: `logs/agent_ann_smoke.log` diff --git a/scripts/README_evidence_hybrid.md b/scripts/README_evidence_hybrid.md new file mode 100644 index 0000000000000000000000000000000000000000..171e3bb36b31d18be3d5f82b696d2bbcadc401a8 --- /dev/null +++ b/scripts/README_evidence_hybrid.md @@ -0,0 +1,16 @@ +# Live EVIDENCE-index Postgres hybrid smoke + +Product indexes / retrieve: `sql/001_agent_and_evidence_indexes.sql`, +`sql/002_evidence_hybrid_retrieve.sql`, fixtures in `sql/003_evidence_seed_cobol.sql` +(embedding left NULL in 003 — loader fills vectors). + +Harness (Python, not product): +- `load_evidence_seed.py` — same fixture IDs as 003 → `evidence`, deterministic hashed + bag-of-tokens → `vector(1536)` via `embed` / `fmt_vector` from `load_agent_registry.py` +- `evidence_hybrid_smoke.py` — COBOL balance hybrid query (dense + lexical + authority; + `min_authority=0.5`; `require_parent=false` — junk dropped by authority floor) + +Done criteria: `ev_copybook_01` / `ev_cobol_src_01` / `ev_db2_schema_01` beat Quant; +`ev_junk_low_auth` dropped at `min_authority=0.5`. + +Measured log: `logs/evidence_hybrid_smoke.log` diff --git a/scripts/agent_ann_smoke.py b/scripts/agent_ann_smoke.py new file mode 100644 index 0000000000000000000000000000000000000000..ca184421a1120f613d7ce4803d6d5ed421f99f6f --- /dev/null +++ b/scripts/agent_ann_smoke.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Harness: embed COBOL balance query, ANN top-K against agent_registry.""" +from __future__ import annotations + +import math +import sys +from pathlib import Path + +# reuse embed from loader +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from load_agent_registry import embed, fmt_vector + +import psycopg2 + +QUERY = ( + "Explain how this COBOL payment program calculates the account balance " + "and determine whether its Java replacement preserves the behavior. " + "cobol copybook business_rule_extraction control_flow data_flow " + "legacy_mainframe modernization_analysis" +) +TOP_K = 5 + + +def main() -> int: + dsn = sys.argv[1] if len(sys.argv) > 1 else "dbname=david user=david host=127.0.0.1" + qvec = fmt_vector(embed(QUERY)) + conn = psycopg2.connect(dsn) + with conn.cursor() as cur: + cur.execute( + """ + SELECT agent_id, domain, risk_class, + 1 - (embedding <=> %s::vector) AS cosine_sim + FROM agent_registry + WHERE embedding IS NOT NULL + ORDER BY embedding <=> %s::vector + LIMIT %s + """, + (qvec, qvec, TOP_K), + ) + rows = cur.fetchall() + conn.close() + + print(f"query={QUERY[:80]}...") + print(f"top_k={TOP_K}") + for i, (aid, domain, risk, sim) in enumerate(rows, 1): + print(f"{i}\t{aid}\t{domain}\t{risk}\tsim={sim:.6f}") + + ids = [r[0] for r in rows] + ok_legacy = "LegacyCobol" in ids + # Quant should not outrank LegacyCobol (prefer: Legacy ahead of Quant if both present) + if "Quant" in ids and "LegacyCobol" in ids: + ok_order = ids.index("LegacyCobol") < ids.index("Quant") + else: + ok_order = "Quant" not in ids[:1] # Quant not the preferred top hit + + print(f"legacy_in_topk={ok_legacy}") + print(f"quant_not_preferred={ok_order}") + print(f"pass={ok_legacy and ok_order}") + return 0 if (ok_legacy and ok_order) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_prolog_registry_sync.sh b/scripts/check_prolog_registry_sync.sh new file mode 100644 index 0000000000000000000000000000000000000000..f9bf4245d2e726f07ee13dbb955369d72f751d66 --- /dev/null +++ b/scripts/check_prolog_registry_sync.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Harness: regenerate registry_facts.pl and fail if dirty vs committed tree. +# Exit 0 iff generated facts match router/registry_facts.pl on disk. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +GEN="$ROOT/scripts/gen_registry_facts.py" +TARGET="$ROOT/router/registry_facts.pl" +TMP="$(mktemp "${TMPDIR:-/tmp}/registry_facts.XXXXXX.pl")" +trap 'rm -f "$TMP"' EXIT + +python3 "$GEN" "$TMP" +if ! cmp -s "$TMP" "$TARGET"; then + echo "FAIL: router/registry_facts.pl out of sync with registry/seed_agents.yaml" >&2 + echo "Diff (generated vs on-disk):" >&2 + diff -u "$TARGET" "$TMP" >&2 || true + echo "Re-run: python3 scripts/gen_registry_facts.py" >&2 + exit 1 +fi + +# Also require that symbolic_rules.pl includes the generated facts. +if ! grep -q "include('registry_facts.pl')" "$ROOT/router/symbolic_rules.pl" \ + && ! grep -q 'include("registry_facts.pl")' "$ROOT/router/symbolic_rules.pl" \ + && ! grep -q "include(registry_facts)" "$ROOT/router/symbolic_rules.pl"; then + echo "FAIL: symbolic_rules.pl must include registry_facts.pl" >&2 + exit 1 +fi + +# Spot-check: every agent_id from YAML appears as agent_id/2 in facts. +python3 - <<'PY' "$ROOT" +import re, sys +from pathlib import Path +root = Path(sys.argv[1]) +yaml_text = (root / "registry" / "seed_agents.yaml").read_text() +facts = (root / "router" / "registry_facts.pl").read_text() +ids = re.findall(r"(?m)^-\s*agent_id:\s*(\S+)", yaml_text) +missing = [i for i in ids if f"agent_id('{i}'" not in facts] +if missing: + print("FAIL: missing agent_id/2 for:", ", ".join(missing), file=sys.stderr) + sys.exit(1) +if "agent(quant)." not in facts: + print("FAIL: Quant negative agent missing", file=sys.stderr) + sys.exit(1) +print(f"sync ok: {len(ids)} agents; Quant present as negative") +PY + +echo "prolog_registry_sync: OK" diff --git a/scripts/evidence_hybrid_smoke.py b/scripts/evidence_hybrid_smoke.py new file mode 100644 index 0000000000000000000000000000000000000000..5943450b547375aa2faacf2348b41311376eef46 --- /dev/null +++ b/scripts/evidence_hybrid_smoke.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Harness: embed COBOL balance query, hybrid retrieve against evidence (sql/002).""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from load_agent_registry import embed, fmt_vector +from load_evidence_seed import normalize_for_embed + +import psycopg2 + +QUERY = ( + "Explain how this COBOL payment program calculates the account balance " + "and determine whether its Java replacement preserves the behavior. " + "cobol payment balance account CALC-BALANCE copybook" +) +# Lexical param: keyword phrase so plainto_tsquery AND-gate can fire on fixtures +# (full prose AND-query matches almost nothing). Dense still uses full QUERY. +LEXICAL_QUERY = "payment balance account" +TOP_K = 5 +MIN_AUTHORITY = 0.5 +# Junk has empty parents AND low auth; min_authority alone drops it. +REQUIRE_PARENT = False + +# Product hybrid SQL (equivalent to sql/002_evidence_hybrid_retrieve.sql) +HYBRID_SQL = """ +SELECT + evidence_id, + source_id, + source_type, + document_id, + location, + content, + content_hash, + source_authority, + parent_evidence, + (1.0 - (embedding <=> %s::vector)) AS dense, + ts_rank_cd(content_tsv, plainto_tsquery('english', %s)) AS lexical, + source_authority AS authority, + CASE + WHEN COALESCE(%s::boolean, false) + AND cardinality(parent_evidence) = 0 + THEN 1.0 + ELSE 0.0 + END AS parent_penalty, + ( + (1.0 - (embedding <=> %s::vector)) + + ts_rank_cd(content_tsv, plainto_tsquery('english', %s)) + + source_authority + - CASE + WHEN COALESCE(%s::boolean, false) + AND cardinality(parent_evidence) = 0 + THEN 1.0 + ELSE 0.0 + END + ) AS score +FROM evidence +WHERE + source_authority >= COALESCE(%s::real, 0.0) + AND ( + %s::text[] IS NULL + OR cardinality(%s::text[]) = 0 + OR source_type = ANY (%s::text[]) + ) + AND ( + NOT COALESCE(%s::boolean, false) + OR cardinality(parent_evidence) > 0 + ) + AND ( + content_tsv @@ plainto_tsquery('english', %s) + OR embedding IS NOT NULL + ) +ORDER BY score DESC, evidence_id +LIMIT GREATEST(COALESCE(%s::int, 10), 0) +""" + +LEGACY_IDS = ("ev_copybook_01", "ev_cobol_src_01", "ev_db2_schema_01") +QUANT_ID = "ev_quant_pricing_01" +JUNK_ID = "ev_junk_low_auth" + + +def main() -> int: + dsn = sys.argv[1] if len(sys.argv) > 1 else "dbname=david user=david host=127.0.0.1" + qvec = fmt_vector(embed(normalize_for_embed(QUERY))) + conn = psycopg2.connect(dsn) + with conn.cursor() as cur: + cur.execute( + "SELECT evidence_id FROM evidence WHERE evidence_id = %s", (JUNK_ID,) + ) + junk_exists = cur.fetchone() is not None + + cur.execute( + HYBRID_SQL, + ( + qvec, + LEXICAL_QUERY, + REQUIRE_PARENT, + qvec, + LEXICAL_QUERY, + REQUIRE_PARENT, + MIN_AUTHORITY, + None, + None, + None, + REQUIRE_PARENT, + LEXICAL_QUERY, + TOP_K, + ), + ) + rows = cur.fetchall() + + cur.execute( + """ + SELECT evidence_id FROM evidence + WHERE source_authority >= %s + AND evidence_id = %s + """, + (MIN_AUTHORITY, JUNK_ID), + ) + junk_in_auth_filter = cur.fetchone() is not None + conn.close() + + print(f"query={QUERY[:80]}...") + print(f"lexical_query={LEXICAL_QUERY}") + print(f"top_k={TOP_K} min_authority={MIN_AUTHORITY} require_parent={REQUIRE_PARENT}") + print("rank\tevidence_id\tsource_type\tscore\tdense\tlexical\tauthority") + ranked = [] + for i, row in enumerate(rows, 1): + eid = row[0] + stype = row[2] + dense = float(row[9]) + lexical = float(row[10]) + authority = float(row[11]) + score = float(row[13]) + ranked.append(eid) + print( + f"{i}\t{eid}\t{stype}\tscore={score:.6f}\t" + f"dense={dense:.6f}\tlexical={lexical:.6f}\tauthority={authority:.6f}" + ) + + junk_dropped = junk_exists and (JUNK_ID not in ranked) and (not junk_in_auth_filter) + quant_not_first = QUANT_ID not in ranked or ranked[0] != QUANT_ID + + legacy_beat_quant = True + for lid in LEGACY_IDS: + if lid in ranked and QUANT_ID in ranked: + if ranked.index(lid) > ranked.index(QUANT_ID): + legacy_beat_quant = False + break + elif QUANT_ID in ranked and lid not in ranked: + legacy_beat_quant = False + break + + legacy_in_topk = all(lid in ranked for lid in LEGACY_IDS) + if QUANT_ID not in ranked: + legacy_beat_quant = legacy_in_topk or any(lid in ranked for lid in LEGACY_IDS) + + ok = junk_dropped and quant_not_first and legacy_beat_quant and legacy_in_topk + + print(f"junk_exists={junk_exists}") + print(f"junk_dropped={junk_dropped}") + print(f"legacy_in_topk={legacy_in_topk}") + print(f"quant_not_preferred={quant_not_first and legacy_beat_quant}") + print(f"pass={ok}") + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/gen_registry_facts.py b/scripts/gen_registry_facts.py new file mode 100644 index 0000000000000000000000000000000000000000..0439f4fff29b6dec190a5a17c001757f8ce71ed0 --- /dev/null +++ b/scripts/gen_registry_facts.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Harness: registry/seed_agents.yaml → router/registry_facts.pl (stdlib + PyYAML). + +Not product code. Regenerates Prolog agent facts so symbolic_rules.pl stays +aligned with the seed registry. Prefer: scripts/check_prolog_registry_sync.sh +""" +from __future__ import annotations + +import sys +from pathlib import Path + +try: + import yaml +except ImportError: + yaml = None + +ROOT = Path(__file__).resolve().parents[1] +SEED = ROOT / "registry" / "seed_agents.yaml" +OUT = ROOT / "router" / "registry_facts.pl" + + +def to_atom(agent_id: str) -> str: + """Registry PascalCase id → Prolog atom (downcase first letter).""" + if not agent_id: + raise ValueError("empty agent_id") + return agent_id[0].lower() + agent_id[1:] + + +def parse_seed_stdlib(text: str) -> list[dict]: + """Minimal YAML subset parser for seed_agents.yaml (no PyYAML required).""" + agents: list[dict] = [] + cur: dict | None = None + list_key: str | None = None + + def flush(): + nonlocal cur + if cur is not None: + agents.append(cur) + cur = None + + for raw in text.splitlines(): + if not raw.strip() or raw.lstrip().startswith("#"): + continue + if raw.startswith("- agent_id:"): + flush() + cur = { + "agent_id": raw.split(":", 1)[1].strip(), + "capabilities": [], + "permissions": [], + "dependencies": [], + "control_plane": False, + } + list_key = None + continue + if cur is None: + continue + indent = len(raw) - len(raw.lstrip(" ")) + s = raw.strip() + if indent == 2 and ":" in s and not s.startswith("-"): + key, _, val = s.partition(":") + key = key.strip() + val = val.strip() + list_key = None + if key in ("capabilities", "permissions", "dependencies", "languages", + "input_types", "output_types", "required_evidence"): + if val.startswith("[") and val.endswith("]"): + inner = val[1:-1].strip() + cur[key] = [x.strip() for x in inner.split(",") if x.strip()] if inner else [] + elif val == "": + list_key = key + cur[key] = [] + else: + cur[key] = [val] + elif key == "control_plane": + cur[key] = val.lower() in ("true", "yes", "1") + elif key == "risk_class": + cur[key] = val + elif key == "domain": + cur[key] = val + else: + cur[key] = val + elif indent >= 4 and s.startswith("- ") and list_key: + cur.setdefault(list_key, []).append(s[2:].strip()) + flush() + return agents + + +def load_agents() -> list[dict]: + text = SEED.read_text(encoding="utf-8") + if yaml is not None: + data = yaml.safe_load(text) + if not isinstance(data, list): + raise SystemExit("seed_agents.yaml: expected list") + return data + return parse_seed_stdlib(text) + + +def emit(agents: list[dict]) -> str: + lines: list[str] = [ + "% SPDX-License-Identifier: AGPL-3.0-only", + "% Copyright (C) 2026 David contributors", + "% GENERATED by scripts/gen_registry_facts.py from registry/seed_agents.yaml", + "% Do not edit by hand — regenerate via scripts/check_prolog_registry_sync.sh", + "%", + "% Agent atom map: registry agent_id → Prolog atom = downcase first letter", + "% LegacyCobol → legacyCobol, Risk → risk, Quant → quant", + "% Predicates: agent_id/2, agent/1, control_plane/1, capability/2,", + "% permission/2, risk_class/2, depends/2", + "", + ] + # agent_id/2 map first + for a in agents: + rid = a["agent_id"] + atom = to_atom(rid) + lines.append(f"agent_id('{rid}', {atom}).") + lines.append("") + for a in agents: + atom = to_atom(a["agent_id"]) + lines.append(f"agent({atom}).") + lines.append("") + for a in agents: + if a.get("control_plane"): + lines.append(f"control_plane({to_atom(a['agent_id'])}).") + lines.append("") + for a in agents: + atom = to_atom(a["agent_id"]) + for cap in a.get("capabilities") or []: + lines.append(f"capability({atom}, {cap}).") + lines.append("") + for a in agents: + atom = to_atom(a["agent_id"]) + for perm in a.get("permissions") or []: + lines.append(f"permission({atom}, {perm}).") + lines.append("") + for a in agents: + atom = to_atom(a["agent_id"]) + rc = a.get("risk_class") + if not rc: + raise SystemExit(f"missing risk_class for {a['agent_id']}") + lines.append(f"risk_class({atom}, {rc}).") + lines.append("") + for a in agents: + atom = to_atom(a["agent_id"]) + for dep in a.get("dependencies") or []: + lines.append(f"depends({atom}, {to_atom(dep)}).") + lines.append("") + return "\n".join(lines) + + +def main() -> int: + out = Path(sys.argv[1]) if len(sys.argv) > 1 else OUT + agents = load_agents() + if not agents: + print("no agents loaded", file=sys.stderr) + return 1 + text = emit(agents) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(text, encoding="utf-8") + print(f"wrote {out} ({len(agents)} agents)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/load_agent_registry.py b/scripts/load_agent_registry.py new file mode 100644 index 0000000000000000000000000000000000000000..0eb01676f1ea4d2ab55d646bc233d76d2d33d334 --- /dev/null +++ b/scripts/load_agent_registry.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Harness: seed_agents.yaml → agent_registry with deterministic 1536-d vectors.""" +from __future__ import annotations + +import hashlib +import math +import re +import sys +from pathlib import Path + +import yaml + +try: + import psycopg2 + from psycopg2.extras import execute_values +except ImportError: + psycopg2 = None + +ROOT = Path(__file__).resolve().parents[1] +SEED = ROOT / "registry" / "seed_agents.yaml" +DIM = 1536 + +TOKEN_RE = re.compile(r"[a-z0-9_]+") + + +def embed_text_of(agent: dict) -> str: + parts = [ + agent["agent_id"], + agent.get("domain", ""), + " ".join(agent.get("capabilities") or []), + " ".join(agent.get("languages") or []), + " ".join(agent.get("input_types") or []), + " ".join(agent.get("output_types") or []), + " ".join(agent.get("dependencies") or []), + agent.get("risk_class", ""), + ] + return " ".join(p for p in parts if p).lower() + + +def embed(text: str, dim: int = DIM) -> list[float]: + """Deterministic hashed bag-of-tokens → L2-normalized vector(dim).""" + vec = [0.0] * dim + toks = TOKEN_RE.findall(text.lower()) + if not toks: + vec[0] = 1.0 + return vec + for tok in toks: + h = hashlib.sha256(tok.encode()).digest() + # two lanes per token for a bit of spread + for off in (0, 8): + idx = int.from_bytes(h[off : off + 4], "little") % dim + sign = 1.0 if h[off + 4] & 1 else -1.0 + vec[idx] += sign + norm = math.sqrt(sum(v * v for v in vec)) or 1.0 + return [v / norm for v in vec] + + +def fmt_vector(v: list[float]) -> str: + return "[" + ",".join(f"{x:.8f}" for x in v) + "]" + + +def main() -> int: + agents = yaml.safe_load(SEED.read_text()) + dsn = sys.argv[1] if len(sys.argv) > 1 else "dbname=david user=david host=127.0.0.1" + if psycopg2 is None: + print("psycopg2 missing", file=sys.stderr) + return 2 + + rows = [] + for a in agents: + et = embed_text_of(a) + emb = embed(et) + rows.append( + ( + a["agent_id"], + a["domain"], + a.get("capabilities") or [], + a.get("languages") or [], + a.get("input_types") or [], + a.get("output_types") or [], + a.get("required_evidence") or [], + a.get("permissions") or [], + a["risk_class"], + a.get("dependencies") or [], + a["execution_endpoint"], + bool(a.get("control_plane", False)), + et, + fmt_vector(emb), + float(a.get("historical_success", 0.5)), + ) + ) + + conn = psycopg2.connect(dsn) + conn.autocommit = True + with conn.cursor() as cur: + cur.execute("TRUNCATE agent_registry") + sql = """ + INSERT INTO agent_registry ( + agent_id, domain, capabilities, languages, input_types, output_types, + required_evidence, permissions, risk_class, dependencies, + execution_endpoint, control_plane, embed_text, embedding, historical_success + ) VALUES %s + """ + template = ( + "(%s,%s,%s::text[],%s::text[],%s::text[],%s::text[]," + "%s::text[],%s::text[],%s,%s::text[],%s,%s,%s,%s::vector,%s)" + ) + execute_values(cur, sql, rows, template=template) + cur.execute("SELECT count(*) FROM agent_registry") + n = cur.fetchone()[0] + conn.close() + print(f"loaded_agents={n}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/load_evidence_seed.py b/scripts/load_evidence_seed.py new file mode 100644 index 0000000000000000000000000000000000000000..4a506eb055140de2023ad703f5b6f3ce1e5799fe --- /dev/null +++ b/scripts/load_evidence_seed.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Harness: COBOL evidence fixtures → evidence with deterministic 1536-d vectors. + +Mirrors sql/003_evidence_seed_cobol.sql IDs/content; fills embedding (003 leaves NULL). +Reuse embed/fmt_vector from load_agent_registry (smoke hashed bag-of-tokens only). +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from load_agent_registry import embed, fmt_vector + +try: + import psycopg2 +except ImportError: + psycopg2 = None + +# Same fixtures as sql/003_evidence_seed_cobol.sql +FIXTURES = [ + { + "evidence_id": "ev_copybook_01", + "source_id": "src_copybook_acct", + "source_type": "copybook", + "document_id": "ACCTREC.cpy", + "location": "01-ACCOUNT-BALANCE", + "content": ( + "01 ACCOUNT-RECORD. 05 ACCOUNT-BALANCE PIC S9(9)V99. " + "05 PAYMENT-AMOUNT PIC S9(7)V99. 05 LAST-PAYMENT-DATE PIC X(8)." + ), + "content_hash": "seed_copybook_01", + "source_authority": 0.95, + "parent_evidence": ["ev_provenance_root"], + }, + { + "evidence_id": "ev_cobol_src_01", + "source_id": "src_cobol_pay", + "source_type": "cobol_source", + "document_id": "PAYROLL.cbl", + "location": "PROCEDURE DIVISION / CALC-BALANCE", + "content": ( + "COMPUTE ACCOUNT-BALANCE = ACCOUNT-BALANCE - PAYMENT-AMOUNT. " + "IF ACCOUNT-BALANCE < ZERO THEN PERFORM OVERDRAFT-CHECK. " + "MOVE PAYMENT-AMOUNT TO WS-LAST-PAYMENT." + ), + "content_hash": "seed_cobol_src_01", + "source_authority": 0.90, + "parent_evidence": ["ev_provenance_root"], + }, + { + "evidence_id": "ev_db2_schema_01", + "source_id": "src_db2_acct", + "source_type": "db2_schema", + "document_id": "ACCT.DDL", + "location": "TABLE ACCOUNT_BALANCES", + "content": ( + "CREATE TABLE ACCOUNT_BALANCES (ACCT_ID CHAR(12), BALANCE DECIMAL(11,2), " + "LAST_PAYMENT DECIMAL(9,2), UPDATED_TS TIMESTAMP)." + ), + "content_hash": "seed_db2_schema_01", + "source_authority": 0.85, + "parent_evidence": ["ev_provenance_root"], + }, + { + "evidence_id": "ev_quant_pricing_01", + "source_id": "src_quant_report", + "source_type": "quant_report", + "document_id": "PRICING_Q3.pdf", + "location": "section:monte_carlo", + "content": ( + "Monte Carlo pricing of exotic options. Implied volatility surface and " + "greek sensitivities. No COBOL or account balance fields." + ), + "content_hash": "seed_quant_pricing_01", + "source_authority": 0.80, + "parent_evidence": ["ev_provenance_root"], + }, + { + "evidence_id": "ev_lexical_lift_01", + "source_id": "src_glossary", + "source_type": "glossary", + "document_id": "LEGACY-GLOSSARY.txt", + "location": "entry:PAYMENT-BALANCE", + "content": ( + "cobol payment balance glossary entry: PAYMENT and BALANCE fields on " + "ACCOUNT records in legacy batch." + ), + "content_hash": "seed_lexical_lift_01", + "source_authority": 0.70, + "parent_evidence": ["ev_provenance_root"], + }, + { + "evidence_id": "ev_junk_low_auth", + "source_id": "src_web_scrape", + "source_type": "web_scrape", + "document_id": "random-blog.html", + "location": "body", + "content": ( + "Someone said COBOL payment balance might work like a spreadsheet. " + "Unverified forum post." + ), + "content_hash": "seed_junk_low_auth", + "source_authority": 0.10, + "parent_evidence": [], + }, +] + + +def normalize_for_embed(text: str) -> str: + """Split identifiers on - _ / . so ACCOUNT_BALANCES → account balances tokens.""" + return re.sub(r"[-_./]+", " ", text.lower()) + + +def embed_text_of(f: dict) -> str: + """content + source_type + document_id + location for dense path.""" + parts = [ + f.get("source_type") or "", + f.get("document_id") or "", + f.get("location") or "", + f["content"], + ] + return " ".join(p for p in parts if p) + + +def main() -> int: + dsn = sys.argv[1] if len(sys.argv) > 1 else "dbname=david user=david host=127.0.0.1" + if psycopg2 is None: + print("psycopg2 missing", file=sys.stderr) + return 2 + + conn = psycopg2.connect(dsn) + conn.autocommit = True + with conn.cursor() as cur: + for f in FIXTURES: + et = normalize_for_embed(embed_text_of(f)) + emb = fmt_vector(embed(et)) + cur.execute( + """ + INSERT INTO evidence ( + evidence_id, source_id, source_type, document_id, location, + content, content_hash, embedding, source_authority, parent_evidence + ) VALUES ( + %s, %s, %s, %s, %s, + %s, %s, %s::vector, %s, %s::text[] + ) + ON CONFLICT (evidence_id) DO UPDATE SET + source_id = EXCLUDED.source_id, + source_type = EXCLUDED.source_type, + document_id = EXCLUDED.document_id, + location = EXCLUDED.location, + content = EXCLUDED.content, + content_hash = EXCLUDED.content_hash, + embedding = EXCLUDED.embedding, + source_authority = EXCLUDED.source_authority, + parent_evidence = EXCLUDED.parent_evidence + """, + ( + f["evidence_id"], + f["source_id"], + f["source_type"], + f["document_id"], + f["location"], + f["content"], + f["content_hash"], + emb, + f["source_authority"], + f["parent_evidence"], + ), + ) + cur.execute( + "SELECT count(*) FILTER (WHERE embedding IS NOT NULL), count(*) FROM evidence" + ) + with_emb, total = cur.fetchone() + conn.close() + print(f"loaded_evidence={total} with_embedding={with_emb}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/prolog_selected_handoff.sh b/scripts/prolog_selected_handoff.sh new file mode 100644 index 0000000000000000000000000000000000000000..8145ac5339236f5b152da04626080075c925cf44 --- /dev/null +++ b/scripts/prolog_selected_handoff.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# Materialize Prolog smoke_cobol selection as one agent_id per line. +set -e +cd "$(dirname "$0")/.." +swipl -q -s router/symbolic_rules.pl -g smoke_cobol -t halt > logs/prolog_selected_cobol.raw +sed -n 's/^selected:\[\(.*\)\]/\1/p' logs/prolog_selected_cobol.raw | tr ',' '\n' | tr -d ' []' | sed '/^$/d' > logs/selected_cobol.txt diff --git a/sql/001_agent_and_evidence_indexes.sql b/sql/001_agent_and_evidence_indexes.sql new file mode 100644 index 0000000000000000000000000000000000000000..c5bf26e36a1602b5f1d30d633f20b9b5888e6f39 --- /dev/null +++ b/sql/001_agent_and_evidence_indexes.sql @@ -0,0 +1,58 @@ +-- Synthetic David: separate AGENT and EVIDENCE vector spaces (pgvector) +CREATE EXTENSION IF NOT EXISTS vector; +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +-- AGENT INDEX — "Who should work?" +CREATE TABLE IF NOT EXISTS agent_registry ( + agent_id text PRIMARY KEY, + domain text NOT NULL, + capabilities text[] NOT NULL DEFAULT '{}', + languages text[] NOT NULL DEFAULT '{}', + input_types text[] NOT NULL DEFAULT '{}', + output_types text[] NOT NULL DEFAULT '{}', + required_evidence text[] NOT NULL DEFAULT '{}', + permissions text[] NOT NULL DEFAULT '{}', + risk_class text NOT NULL CHECK (risk_class IN ('low','medium','high','critical')), + dependencies text[] NOT NULL DEFAULT '{}', + execution_endpoint text NOT NULL, + control_plane boolean NOT NULL DEFAULT false, + embed_text text NOT NULL, + embedding vector(1536), + historical_success real NOT NULL DEFAULT 0.0, + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS agent_registry_embedding_hnsw + ON agent_registry USING hnsw (embedding vector_cosine_ops); + +CREATE INDEX IF NOT EXISTS agent_registry_capabilities_gin + ON agent_registry USING gin (capabilities); + +-- EVIDENCE INDEX — "What evidence should they use?" +CREATE TABLE IF NOT EXISTS evidence ( + evidence_id text PRIMARY KEY, + source_id text NOT NULL, + source_type text NOT NULL, + document_id text, + location text, + content text NOT NULL, + content_hash text NOT NULL, + embedding vector(1536), + content_tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED, + source_authority real NOT NULL DEFAULT 0.5, + retrieved_by text, + retrieval_score real, + parent_evidence text[] NOT NULL DEFAULT '{}', + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS evidence_embedding_hnsw + ON evidence USING hnsw (embedding vector_cosine_ops); + +CREATE INDEX IF NOT EXISTS evidence_content_tsv_gin + ON evidence USING gin (content_tsv); + +-- Hybrid evidence retrieval sketch: +-- dense: ORDER BY embedding <=> $query_vec +-- lexical: content_tsv @@ plainto_tsquery('english', $q) +-- then metadata + provenance filters, then rerank. diff --git a/sql/002_evidence_hybrid_retrieve.sql b/sql/002_evidence_hybrid_retrieve.sql new file mode 100644 index 0000000000000000000000000000000000000000..dd3580490bf9ced09f7f8a91f0ba9a52958cb4a2 --- /dev/null +++ b/sql/002_evidence_hybrid_retrieve.sql @@ -0,0 +1,80 @@ +-- Synthetic David — product hybrid retrieve against `evidence` +-- Schema: sql/001_agent_and_evidence_indexes.sql +-- +-- Parameters (bind as $1..$n; comments document intent): +-- $1 query_embedding vector(1536) -- dense cosine query +-- $2 query_text text -- lexical plainto_tsquery input +-- $3 source_types text[] -- optional; NULL / empty = no filter +-- $4 min_authority real -- provenance floor on source_authority +-- $5 require_parent boolean -- if true, parent_evidence must be non-empty +-- $6 top_k int -- LIMIT +-- +-- Score weights (deterministic merge / rerank — no dense floor): +-- dense = 1 - (embedding <=> $1) -- cosine similarity from cosine distance +-- lexical = ts_rank_cd(content_tsv, plainto_tsquery('english', $2)) +-- authority = source_authority +-- penalty = CASE WHEN $5 AND cardinality(parent_evidence)=0 THEN 1.0 ELSE 0.0 END +-- score = dense + lexical + authority - penalty +-- +-- ORDER BY score DESC, evidence_id (stable tie-break) +-- Hybrid note: weak dense + strong lexical can still rank (no hard dense floor). + +SELECT + evidence_id, + source_id, + source_type, + document_id, + location, + content, + content_hash, + source_authority, + parent_evidence, + -- component scores for retrieval_metadata / provenance + (1.0 - (embedding <=> $1::vector)) AS dense, -- weight 1.0 + ts_rank_cd(content_tsv, plainto_tsquery('english', $2)) AS lexical, -- weight 1.0 + source_authority AS authority, -- weight 1.0 + CASE + WHEN COALESCE($5::boolean, false) + AND cardinality(parent_evidence) = 0 + THEN 1.0 + ELSE 0.0 + END AS parent_penalty, -- weight 1.0 + ( + (1.0 - (embedding <=> $1::vector)) + + ts_rank_cd(content_tsv, plainto_tsquery('english', $2)) + + source_authority + - CASE + WHEN COALESCE($5::boolean, false) + AND cardinality(parent_evidence) = 0 + THEN 1.0 + ELSE 0.0 + END + ) AS score +FROM evidence +WHERE + -- provenance: authority floor + source_authority >= COALESCE($4::real, 0.0) + -- optional metadata: source_type = ANY(...) + AND ( + $3::text[] IS NULL + OR cardinality($3::text[]) = 0 + OR source_type = ANY ($3::text[]) + ) + -- optional provenance: require non-empty parent_evidence + AND ( + NOT COALESCE($5::boolean, false) + OR cardinality(parent_evidence) > 0 + ) + -- lexical gate (rows with zero tsquery match still allowed via dense path: + -- keep OR so weak-dense / strong-lexical AND strong-dense / weak-lexical both survive) + AND ( + content_tsv @@ plainto_tsquery('english', $2) + OR embedding IS NOT NULL + ) +ORDER BY score DESC, evidence_id +LIMIT GREATEST(COALESCE($6::int, 10), 0); + +-- Dense-only sketch (reference): +-- ORDER BY embedding <=> $1 +-- Lexical-only sketch (reference): +-- WHERE content_tsv @@ plainto_tsquery('english', $2) diff --git a/sql/003_evidence_seed_cobol.sql b/sql/003_evidence_seed_cobol.sql new file mode 100644 index 0000000000000000000000000000000000000000..e2976994eb90b094dd1c3813e83d8799ca3db4ed --- /dev/null +++ b/sql/003_evidence_seed_cobol.sql @@ -0,0 +1,81 @@ +-- Synthetic David — COBOL modernization seed fixture (mirrors j/evidence.ijs) +-- embedding left NULL until an encoder exists; content/authority/parent drive smoke filters. +-- Fixture evidence_ids match pure-J seed corpus. + +INSERT INTO evidence ( + evidence_id, source_id, source_type, document_id, location, + content, content_hash, embedding, source_authority, parent_evidence +) VALUES +( + 'ev_copybook_01', + 'src_copybook_acct', + 'copybook', + 'ACCTREC.cpy', + '01-ACCOUNT-BALANCE', + '01 ACCOUNT-RECORD. 05 ACCOUNT-BALANCE PIC S9(9)V99. 05 PAYMENT-AMOUNT PIC S9(7)V99. 05 LAST-PAYMENT-DATE PIC X(8).', + 'seed_copybook_01', + NULL, -- vector(1536) when encoder exists; J smoke uses dim-8 stand-in + 0.95, + ARRAY['ev_provenance_root'] +), +( + 'ev_cobol_src_01', + 'src_cobol_pay', + 'cobol_source', + 'PAYROLL.cbl', + 'PROCEDURE DIVISION / CALC-BALANCE', + 'COMPUTE ACCOUNT-BALANCE = ACCOUNT-BALANCE - PAYMENT-AMOUNT. IF ACCOUNT-BALANCE < ZERO THEN PERFORM OVERDRAFT-CHECK. MOVE PAYMENT-AMOUNT TO WS-LAST-PAYMENT.', + 'seed_cobol_src_01', + NULL, + 0.90, + ARRAY['ev_provenance_root'] +), +( + 'ev_db2_schema_01', + 'src_db2_acct', + 'db2_schema', + 'ACCT.DDL', + 'TABLE ACCOUNT_BALANCES', + 'CREATE TABLE ACCOUNT_BALANCES (ACCT_ID CHAR(12), BALANCE DECIMAL(11,2), LAST_PAYMENT DECIMAL(9,2), UPDATED_TS TIMESTAMP).', + 'seed_db2_schema_01', + NULL, + 0.85, + ARRAY['ev_provenance_root'] +), +( + 'ev_quant_pricing_01', + 'src_quant_report', + 'quant_report', + 'PRICING_Q3.pdf', + 'section:monte_carlo', + 'Monte Carlo pricing of exotic options. Implied volatility surface and greek sensitivities. No COBOL or account balance fields.', + 'seed_quant_pricing_01', + NULL, + 0.80, + ARRAY['ev_provenance_root'] +), +( + 'ev_lexical_lift_01', + 'src_glossary', + 'glossary', + 'LEGACY-GLOSSARY.txt', + 'entry:PAYMENT-BALANCE', + 'cobol payment balance glossary entry: PAYMENT and BALANCE fields on ACCOUNT records in legacy batch.', + 'seed_lexical_lift_01', + NULL, + 0.70, + ARRAY['ev_provenance_root'] +), +( + 'ev_junk_low_auth', + 'src_web_scrape', + 'web_scrape', + 'random-blog.html', + 'body', + 'Someone said COBOL payment balance might work like a spreadsheet. Unverified forum post.', + 'seed_junk_low_auth', + NULL, + 0.10, + ARRAY[]::text[] +) +ON CONFLICT (evidence_id) DO NOTHING;