October 2026 main drop: mirror from GitHub
Browse filesThis view is limited to 50 files because it contains too many changes. Β See raw diff
- .gitattributes +4 -0
- .gitignore +6 -0
- LICENSE +661 -0
- README.md +17 -0
- docs/contracts.md +45 -0
- docs/images/.gitkeep +0 -0
- docs/images/david-demos.png +3 -0
- docs/images/david-evidence.png +3 -0
- docs/images/david-execution.png +3 -0
- docs/images/david-prolog.png +3 -0
- evidence/__init__.py +17 -0
- evidence/retrieve.py +346 -0
- evidence/test_retrieve.py +160 -0
- evidence/write_router_smoke.py +133 -0
- execution/__init__.py +20 -0
- execution/dispatcher.py +99 -0
- execution/models.py +66 -0
- execution/test_dispatcher.py +97 -0
- j/README.md +26 -0
- j/eligibility.ijs +93 -0
- j/evidence.ijs +218 -0
- j/execution.ijs +203 -0
- j/fixtures/ann_topk.txt +8 -0
- j/run_eligibility_smoke.ijs +33 -0
- j/run_evidence_smoke.ijs +5 -0
- j/run_execution_smoke.ijs +9 -0
- j/run_smoke.ijs +5 -0
- j/scorer.ijs +65 -0
- j/symbolic_call.ijs +7 -0
- logs/agent_ann_smoke.log +10 -0
- logs/evidence_hybrid_smoke.log +14 -0
- logs/j_evidence_smoke.log +6 -0
- logs/j_execution_smoke.log +3 -0
- logs/prolog_eligibility_smoke.log +7 -0
- logs/prolog_registry_sync.log +3 -0
- logs/prolog_selected_cobol.raw +2 -0
- logs/prolog_smoke_cobol.log +2 -0
- logs/router_smoke.log +4 -0
- logs/selected_cobol.txt +6 -0
- registry/agent_descriptor.schema.json +69 -0
- registry/seed_agents.yaml +132 -0
- router/registry_facts.pl +98 -0
- router/scorer.py +147 -0
- router/symbolic.py +153 -0
- router/symbolic_rules.pl +203 -0
- router/test_scorer.py +71 -0
- router/test_symbolic.py +124 -0
- scripts/README_agent_ann.md +11 -0
- scripts/README_evidence_hybrid.md +16 -0
- scripts/agent_ann_smoke.py +63 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
docs/images/david-demos.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
docs/images/david-evidence.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
docs/images/david-execution.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
docs/images/david-prolog.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*.egg-info/
|
| 4 |
+
.Python
|
| 5 |
+
.venv/
|
| 6 |
+
venv/
|
LICENSE
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
| 2 |
+
Version 3, 19 November 2007
|
| 3 |
+
|
| 4 |
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
| 5 |
+
Everyone is permitted to copy and distribute verbatim copies
|
| 6 |
+
of this license document, but changing it is not allowed.
|
| 7 |
+
|
| 8 |
+
Preamble
|
| 9 |
+
|
| 10 |
+
The GNU Affero General Public License is a free, copyleft license for
|
| 11 |
+
software and other kinds of works, specifically designed to ensure
|
| 12 |
+
cooperation with the community in the case of network server software.
|
| 13 |
+
|
| 14 |
+
The licenses for most software and other practical works are designed
|
| 15 |
+
to take away your freedom to share and change the works. By contrast,
|
| 16 |
+
our General Public Licenses are intended to guarantee your freedom to
|
| 17 |
+
share and change all versions of a program--to make sure it remains free
|
| 18 |
+
software for all its users.
|
| 19 |
+
|
| 20 |
+
When we speak of free software, we are referring to freedom, not
|
| 21 |
+
price. Our General Public Licenses are designed to make sure that you
|
| 22 |
+
have the freedom to distribute copies of free software (and charge for
|
| 23 |
+
them if you wish), that you receive source code or can get it if you
|
| 24 |
+
want it, that you can change the software or use pieces of it in new
|
| 25 |
+
free programs, and that you know you can do these things.
|
| 26 |
+
|
| 27 |
+
Developers that use our General Public Licenses protect your rights
|
| 28 |
+
with two steps: (1) assert copyright on the software, and (2) offer
|
| 29 |
+
you this License which gives you legal permission to copy, distribute
|
| 30 |
+
and/or modify the software.
|
| 31 |
+
|
| 32 |
+
A secondary benefit of defending all users' freedom is that
|
| 33 |
+
improvements made in alternate versions of the program, if they
|
| 34 |
+
receive widespread use, become available for other developers to
|
| 35 |
+
incorporate. Many developers of free software are heartened and
|
| 36 |
+
encouraged by the resulting cooperation. However, in the case of
|
| 37 |
+
software used on network servers, this result may fail to come about.
|
| 38 |
+
The GNU General Public License permits making a modified version and
|
| 39 |
+
letting the public access it on a server without ever releasing its
|
| 40 |
+
source code to the public.
|
| 41 |
+
|
| 42 |
+
The GNU Affero General Public License is designed specifically to
|
| 43 |
+
ensure that, in such cases, the modified source code becomes available
|
| 44 |
+
to the community. It requires the operator of a network server to
|
| 45 |
+
provide the source code of the modified version running there to the
|
| 46 |
+
users of that server. Therefore, public use of a modified version, on
|
| 47 |
+
a publicly accessible server, gives the public access to the source
|
| 48 |
+
code of the modified version.
|
| 49 |
+
|
| 50 |
+
An older license, called the Affero General Public License and
|
| 51 |
+
published by Affero, was designed to accomplish similar goals. This is
|
| 52 |
+
a different license, not a version of the Affero GPL, but Affero has
|
| 53 |
+
released a new version of the Affero GPL which permits relicensing under
|
| 54 |
+
this license.
|
| 55 |
+
|
| 56 |
+
The precise terms and conditions for copying, distribution and
|
| 57 |
+
modification follow.
|
| 58 |
+
|
| 59 |
+
TERMS AND CONDITIONS
|
| 60 |
+
|
| 61 |
+
0. Definitions.
|
| 62 |
+
|
| 63 |
+
"This License" refers to version 3 of the GNU Affero General Public License.
|
| 64 |
+
|
| 65 |
+
"Copyright" also means copyright-like laws that apply to other kinds of
|
| 66 |
+
works, such as semiconductor masks.
|
| 67 |
+
|
| 68 |
+
"The Program" refers to any copyrightable work licensed under this
|
| 69 |
+
License. Each licensee is addressed as "you". "Licensees" and
|
| 70 |
+
"recipients" may be individuals or organizations.
|
| 71 |
+
|
| 72 |
+
To "modify" a work means to copy from or adapt all or part of the work
|
| 73 |
+
in a fashion requiring copyright permission, other than the making of an
|
| 74 |
+
exact copy. The resulting work is called a "modified version" of the
|
| 75 |
+
earlier work or a work "based on" the earlier work.
|
| 76 |
+
|
| 77 |
+
A "covered work" means either the unmodified Program or a work based
|
| 78 |
+
on the Program.
|
| 79 |
+
|
| 80 |
+
To "propagate" a work means to do anything with it that, without
|
| 81 |
+
permission, would make you directly or secondarily liable for
|
| 82 |
+
infringement under applicable copyright law, except executing it on a
|
| 83 |
+
computer or modifying a private copy. Propagation includes copying,
|
| 84 |
+
distribution (with or without modification), making available to the
|
| 85 |
+
public, and in some countries other activities as well.
|
| 86 |
+
|
| 87 |
+
To "convey" a work means any kind of propagation that enables other
|
| 88 |
+
parties to make or receive copies. Mere interaction with a user through
|
| 89 |
+
a computer network, with no transfer of a copy, is not conveying.
|
| 90 |
+
|
| 91 |
+
An interactive user interface displays "Appropriate Legal Notices"
|
| 92 |
+
to the extent that it includes a convenient and prominently visible
|
| 93 |
+
feature that (1) displays an appropriate copyright notice, and (2)
|
| 94 |
+
tells the user that there is no warranty for the work (except to the
|
| 95 |
+
extent that warranties are provided), that licensees may convey the
|
| 96 |
+
work under this License, and how to view a copy of this License. If
|
| 97 |
+
the interface presents a list of user commands or options, such as a
|
| 98 |
+
menu, a prominent item in the list meets this criterion.
|
| 99 |
+
|
| 100 |
+
1. Source Code.
|
| 101 |
+
|
| 102 |
+
The "source code" for a work means the preferred form of the work
|
| 103 |
+
for making modifications to it. "Object code" means any non-source
|
| 104 |
+
form of a work.
|
| 105 |
+
|
| 106 |
+
A "Standard Interface" means an interface that either is an official
|
| 107 |
+
standard defined by a recognized standards body, or, in the case of
|
| 108 |
+
interfaces specified for a particular programming language, one that
|
| 109 |
+
is widely used among developers working in that language.
|
| 110 |
+
|
| 111 |
+
The "System Libraries" of an executable work include anything, other
|
| 112 |
+
than the work as a whole, that (a) is included in the normal form of
|
| 113 |
+
packaging a Major Component, but which is not part of that Major
|
| 114 |
+
Component, and (b) serves only to enable use of the work with that
|
| 115 |
+
Major Component, or to implement a Standard Interface for which an
|
| 116 |
+
implementation is available to the public in source code form. A
|
| 117 |
+
"Major Component", in this context, means a major essential component
|
| 118 |
+
(kernel, window system, and so on) of the specific operating system
|
| 119 |
+
(if any) on which the executable work runs, or a compiler used to
|
| 120 |
+
produce the work, or an object code interpreter used to run it.
|
| 121 |
+
|
| 122 |
+
The "Corresponding Source" for a work in object code form means all
|
| 123 |
+
the source code needed to generate, install, and (for an executable
|
| 124 |
+
work) run the object code and to modify the work, including scripts to
|
| 125 |
+
control those activities. However, it does not include the work's
|
| 126 |
+
System Libraries, or general-purpose tools or generally available free
|
| 127 |
+
programs which are used unmodified in performing those activities but
|
| 128 |
+
which are not part of the work. For example, Corresponding Source
|
| 129 |
+
includes interface definition files associated with source files for
|
| 130 |
+
the work, and the source code for shared libraries and dynamically
|
| 131 |
+
linked subprograms that the work is specifically designed to require,
|
| 132 |
+
such as by intimate data communication or control flow between those
|
| 133 |
+
subprograms and other parts of the work.
|
| 134 |
+
|
| 135 |
+
The Corresponding Source need not include anything that users
|
| 136 |
+
can regenerate automatically from other parts of the Corresponding
|
| 137 |
+
Source.
|
| 138 |
+
|
| 139 |
+
The Corresponding Source for a work in source code form is that
|
| 140 |
+
same work.
|
| 141 |
+
|
| 142 |
+
2. Basic Permissions.
|
| 143 |
+
|
| 144 |
+
All rights granted under this License are granted for the term of
|
| 145 |
+
copyright on the Program, and are irrevocable provided the stated
|
| 146 |
+
conditions are met. This License explicitly affirms your unlimited
|
| 147 |
+
permission to run the unmodified Program. The output from running a
|
| 148 |
+
covered work is covered by this License only if the output, given its
|
| 149 |
+
content, constitutes a covered work. This License acknowledges your
|
| 150 |
+
rights of fair use or other equivalent, as provided by copyright law.
|
| 151 |
+
|
| 152 |
+
You may make, run and propagate covered works that you do not
|
| 153 |
+
convey, without conditions so long as your license otherwise remains
|
| 154 |
+
in force. You may convey covered works to others for the sole purpose
|
| 155 |
+
of having them make modifications exclusively for you, or provide you
|
| 156 |
+
with facilities for running those works, provided that you comply with
|
| 157 |
+
the terms of this License in conveying all material for which you do
|
| 158 |
+
not control copyright. Those thus making or running the covered works
|
| 159 |
+
for you must do so exclusively on your behalf, under your direction
|
| 160 |
+
and control, on terms that prohibit them from making any copies of
|
| 161 |
+
your copyrighted material outside their relationship with you.
|
| 162 |
+
|
| 163 |
+
Conveying under any other circumstances is permitted solely under
|
| 164 |
+
the conditions stated below. Sublicensing is not allowed; section 10
|
| 165 |
+
makes it unnecessary.
|
| 166 |
+
|
| 167 |
+
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
| 168 |
+
|
| 169 |
+
No covered work shall be deemed part of an effective technological
|
| 170 |
+
measure under any applicable law fulfilling obligations under article
|
| 171 |
+
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
| 172 |
+
similar laws prohibiting or restricting circumvention of such
|
| 173 |
+
measures.
|
| 174 |
+
|
| 175 |
+
When you convey a covered work, you waive any legal power to forbid
|
| 176 |
+
circumvention of technological measures to the extent such circumvention
|
| 177 |
+
is effected by exercising rights under this License with respect to
|
| 178 |
+
the covered work, and you disclaim any intention to limit operation or
|
| 179 |
+
modification of the work as a means of enforcing, against the work's
|
| 180 |
+
users, your or third parties' legal rights to forbid circumvention of
|
| 181 |
+
technological measures.
|
| 182 |
+
|
| 183 |
+
4. Conveying Verbatim Copies.
|
| 184 |
+
|
| 185 |
+
You may convey verbatim copies of the Program's source code as you
|
| 186 |
+
receive it, in any medium, provided that you conspicuously and
|
| 187 |
+
appropriately publish on each copy an appropriate copyright notice;
|
| 188 |
+
keep intact all notices stating that this License and any
|
| 189 |
+
non-permissive terms added in accord with section 7 apply to the code;
|
| 190 |
+
keep intact all notices of the absence of any warranty; and give all
|
| 191 |
+
recipients a copy of this License along with the Program.
|
| 192 |
+
|
| 193 |
+
You may charge any price or no price for each copy that you convey,
|
| 194 |
+
and you may offer support or warranty protection for a fee.
|
| 195 |
+
|
| 196 |
+
5. Conveying Modified Source Versions.
|
| 197 |
+
|
| 198 |
+
You may convey a work based on the Program, or the modifications to
|
| 199 |
+
produce it from the Program, in the form of source code under the
|
| 200 |
+
terms of section 4, provided that you also meet all of these conditions:
|
| 201 |
+
|
| 202 |
+
a) The work must carry prominent notices stating that you modified
|
| 203 |
+
it, and giving a relevant date.
|
| 204 |
+
|
| 205 |
+
b) The work must carry prominent notices stating that it is
|
| 206 |
+
released under this License and any conditions added under section
|
| 207 |
+
7. This requirement modifies the requirement in section 4 to
|
| 208 |
+
"keep intact all notices".
|
| 209 |
+
|
| 210 |
+
c) You must license the entire work, as a whole, under this
|
| 211 |
+
License to anyone who comes into possession of a copy. This
|
| 212 |
+
License will therefore apply, along with any applicable section 7
|
| 213 |
+
additional terms, to the whole of the work, and all its parts,
|
| 214 |
+
regardless of how they are packaged. This License gives no
|
| 215 |
+
permission to license the work in any other way, but it does not
|
| 216 |
+
invalidate such permission if you have separately received it.
|
| 217 |
+
|
| 218 |
+
d) If the work has interactive user interfaces, each must display
|
| 219 |
+
Appropriate Legal Notices; however, if the Program has interactive
|
| 220 |
+
interfaces that do not display Appropriate Legal Notices, your
|
| 221 |
+
work need not make them do so.
|
| 222 |
+
|
| 223 |
+
A compilation of a covered work with other separate and independent
|
| 224 |
+
works, which are not by their nature extensions of the covered work,
|
| 225 |
+
and which are not combined with it such as to form a larger program,
|
| 226 |
+
in or on a volume of a storage or distribution medium, is called an
|
| 227 |
+
"aggregate" if the compilation and its resulting copyright are not
|
| 228 |
+
used to limit the access or legal rights of the compilation's users
|
| 229 |
+
beyond what the individual works permit. Inclusion of a covered work
|
| 230 |
+
in an aggregate does not cause this License to apply to the other
|
| 231 |
+
parts of the aggregate.
|
| 232 |
+
|
| 233 |
+
6. Conveying Non-Source Forms.
|
| 234 |
+
|
| 235 |
+
You may convey a covered work in object code form under the terms
|
| 236 |
+
of sections 4 and 5, provided that you also convey the
|
| 237 |
+
machine-readable Corresponding Source under the terms of this License,
|
| 238 |
+
in one of these ways:
|
| 239 |
+
|
| 240 |
+
a) Convey the object code in, or embodied in, a physical product
|
| 241 |
+
(including a physical distribution medium), accompanied by the
|
| 242 |
+
Corresponding Source fixed on a durable physical medium
|
| 243 |
+
customarily used for software interchange.
|
| 244 |
+
|
| 245 |
+
b) Convey the object code in, or embodied in, a physical product
|
| 246 |
+
(including a physical distribution medium), accompanied by a
|
| 247 |
+
written offer, valid for at least three years and valid for as
|
| 248 |
+
long as you offer spare parts or customer support for that product
|
| 249 |
+
model, to give anyone who possesses the object code either (1) a
|
| 250 |
+
copy of the Corresponding Source for all the software in the
|
| 251 |
+
product that is covered by this License, on a durable physical
|
| 252 |
+
medium customarily used for software interchange, for a price no
|
| 253 |
+
more than your reasonable cost of physically performing this
|
| 254 |
+
conveying of source, or (2) access to copy the
|
| 255 |
+
Corresponding Source from a network server at no charge.
|
| 256 |
+
|
| 257 |
+
c) Convey individual copies of the object code with a copy of the
|
| 258 |
+
written offer to provide the Corresponding Source. This
|
| 259 |
+
alternative is allowed only occasionally and noncommercially, and
|
| 260 |
+
only if you received the object code with such an offer, in accord
|
| 261 |
+
with subsection 6b.
|
| 262 |
+
|
| 263 |
+
d) Convey the object code by offering access from a designated
|
| 264 |
+
place (gratis or for a charge), and offer equivalent access to the
|
| 265 |
+
Corresponding Source in the same way through the same place at no
|
| 266 |
+
further charge. You need not require recipients to copy the
|
| 267 |
+
Corresponding Source along with the object code. If the place to
|
| 268 |
+
copy the object code is a network server, the Corresponding Source
|
| 269 |
+
may be on a different server (operated by you or a third party)
|
| 270 |
+
that supports equivalent copying facilities, provided you maintain
|
| 271 |
+
clear directions next to the object code saying where to find the
|
| 272 |
+
Corresponding Source. Regardless of what server hosts the
|
| 273 |
+
Corresponding Source, you remain obligated to ensure that it is
|
| 274 |
+
available for as long as needed to satisfy these requirements.
|
| 275 |
+
|
| 276 |
+
e) Convey the object code using peer-to-peer transmission, provided
|
| 277 |
+
you inform other peers where the object code and Corresponding
|
| 278 |
+
Source of the work are being offered to the general public at no
|
| 279 |
+
charge under subsection 6d.
|
| 280 |
+
|
| 281 |
+
A separable portion of the object code, whose source code is excluded
|
| 282 |
+
from the Corresponding Source as a System Library, need not be
|
| 283 |
+
included in conveying the object code work.
|
| 284 |
+
|
| 285 |
+
A "User Product" is either (1) a "consumer product", which means any
|
| 286 |
+
tangible personal property which is normally used for personal, family,
|
| 287 |
+
or household purposes, or (2) anything designed or sold for incorporation
|
| 288 |
+
into a dwelling. In determining whether a product is a consumer product,
|
| 289 |
+
doubtful cases shall be resolved in favor of coverage. For a particular
|
| 290 |
+
product received by a particular user, "normally used" refers to a
|
| 291 |
+
typical or common use of that class of product, regardless of the status
|
| 292 |
+
of the particular user or of the way in which the particular user
|
| 293 |
+
actually uses, or expects or is expected to use, the product. A product
|
| 294 |
+
is a consumer product regardless of whether the product has substantial
|
| 295 |
+
commercial, industrial or non-consumer uses, unless such uses represent
|
| 296 |
+
the only significant mode of use of the product.
|
| 297 |
+
|
| 298 |
+
"Installation Information" for a User Product means any methods,
|
| 299 |
+
procedures, authorization keys, or other information required to install
|
| 300 |
+
and execute modified versions of a covered work in that User Product from
|
| 301 |
+
a modified version of its Corresponding Source. The information must
|
| 302 |
+
suffice to ensure that the continued functioning of the modified object
|
| 303 |
+
code is in no case prevented or interfered with solely because
|
| 304 |
+
modification has been made.
|
| 305 |
+
|
| 306 |
+
If you convey an object code work under this section in, or with, or
|
| 307 |
+
specifically for use in, a User Product, and the conveying occurs as
|
| 308 |
+
part of a transaction in which the right of possession and use of the
|
| 309 |
+
User Product is transferred to the recipient in perpetuity or for a
|
| 310 |
+
fixed term (regardless of how the transaction is characterized), the
|
| 311 |
+
Corresponding Source conveyed under this section must be accompanied
|
| 312 |
+
by the Installation Information. But this requirement does not apply
|
| 313 |
+
if neither you nor any third party retains the ability to install
|
| 314 |
+
modified object code on the User Product (for example, the work has
|
| 315 |
+
been installed in ROM).
|
| 316 |
+
|
| 317 |
+
The requirement to provide Installation Information does not include a
|
| 318 |
+
requirement to continue to provide support service, warranty, or updates
|
| 319 |
+
for a work that has been modified or installed by the recipient, or for
|
| 320 |
+
the User Product in which it has been modified or installed. Access to a
|
| 321 |
+
network may be denied when the modification itself materially and
|
| 322 |
+
adversely affects the operation of the network or violates the rules and
|
| 323 |
+
protocols for communication across the network.
|
| 324 |
+
|
| 325 |
+
Corresponding Source conveyed, and Installation Information provided,
|
| 326 |
+
in accord with this section must be in a format that is publicly
|
| 327 |
+
documented (and with an implementation available to the public in
|
| 328 |
+
source code form), and must require no special password or key for
|
| 329 |
+
unpacking, reading or copying.
|
| 330 |
+
|
| 331 |
+
7. Additional Terms.
|
| 332 |
+
|
| 333 |
+
"Additional permissions" are terms that supplement the terms of this
|
| 334 |
+
License by making exceptions from one or more of its conditions.
|
| 335 |
+
Additional permissions that are applicable to the entire Program shall
|
| 336 |
+
be treated as though they were included in this License, to the extent
|
| 337 |
+
that they are valid under applicable law. If additional permissions
|
| 338 |
+
apply only to part of the Program, that part may be used separately
|
| 339 |
+
under those permissions, but the entire Program remains governed by
|
| 340 |
+
this License without regard to the additional permissions.
|
| 341 |
+
|
| 342 |
+
When you convey a copy of a covered work, you may at your option
|
| 343 |
+
remove any additional permissions from that copy, or from any part of
|
| 344 |
+
it. (Additional permissions may be written to require their own
|
| 345 |
+
removal in certain cases when you modify the work.) You may place
|
| 346 |
+
additional permissions on material, added by you to a covered work,
|
| 347 |
+
for which you have or can give appropriate copyright permission.
|
| 348 |
+
|
| 349 |
+
Notwithstanding any other provision of this License, for material you
|
| 350 |
+
add to a covered work, you may (if authorized by the copyright holders of
|
| 351 |
+
that material) supplement the terms of this License with terms:
|
| 352 |
+
|
| 353 |
+
a) Disclaiming warranty or limiting liability differently from the
|
| 354 |
+
terms of sections 15 and 16 of this License; or
|
| 355 |
+
|
| 356 |
+
b) Requiring preservation of specified reasonable legal notices or
|
| 357 |
+
author attributions in that material or in the Appropriate Legal
|
| 358 |
+
Notices displayed by works containing it; or
|
| 359 |
+
|
| 360 |
+
c) Prohibiting misrepresentation of the origin of that material, or
|
| 361 |
+
requiring that modified versions of such material be marked in
|
| 362 |
+
reasonable ways as different from the original version; or
|
| 363 |
+
|
| 364 |
+
d) Limiting the use for publicity purposes of names of licensors or
|
| 365 |
+
authors of the material; or
|
| 366 |
+
|
| 367 |
+
e) Declining to grant rights under trademark law for use of some
|
| 368 |
+
trade names, trademarks, or service marks; or
|
| 369 |
+
|
| 370 |
+
f) Requiring indemnification of licensors and authors of that
|
| 371 |
+
material by anyone who conveys the material (or modified versions of
|
| 372 |
+
it) with contractual assumptions of liability to the recipient, for
|
| 373 |
+
any liability that these contractual assumptions directly impose on
|
| 374 |
+
those licensors and authors.
|
| 375 |
+
|
| 376 |
+
All other non-permissive additional terms are considered "further
|
| 377 |
+
restrictions" within the meaning of section 10. If the Program as you
|
| 378 |
+
received it, or any part of it, contains a notice stating that it is
|
| 379 |
+
governed by this License along with a term that is a further
|
| 380 |
+
restriction, you may remove that term. If a license document contains
|
| 381 |
+
a further restriction but permits relicensing or conveying under this
|
| 382 |
+
License, you may add to a covered work material governed by the terms
|
| 383 |
+
of that license document, provided that the further restriction does
|
| 384 |
+
not survive such relicensing or conveying.
|
| 385 |
+
|
| 386 |
+
If you add terms to a covered work in accord with this section, you
|
| 387 |
+
must place, in the relevant source files, a statement of the
|
| 388 |
+
additional terms that apply to those files, or a notice indicating
|
| 389 |
+
where to find the applicable terms.
|
| 390 |
+
|
| 391 |
+
Additional terms, permissive or non-permissive, may be stated in the
|
| 392 |
+
form of a separately written license, or stated as exceptions;
|
| 393 |
+
the above requirements apply either way.
|
| 394 |
+
|
| 395 |
+
8. Termination.
|
| 396 |
+
|
| 397 |
+
You may not propagate or modify a covered work except as expressly
|
| 398 |
+
provided under this License. Any attempt otherwise to propagate or
|
| 399 |
+
modify it is void, and will automatically terminate your rights under
|
| 400 |
+
this License (including any patent licenses granted under the third
|
| 401 |
+
paragraph of section 11).
|
| 402 |
+
|
| 403 |
+
However, if you cease all violation of this License, then your
|
| 404 |
+
license from a particular copyright holder is reinstated (a)
|
| 405 |
+
provisionally, unless and until the copyright holder explicitly and
|
| 406 |
+
finally terminates your license, and (b) permanently, if the copyright
|
| 407 |
+
holder fails to notify you of the violation by some reasonable means
|
| 408 |
+
prior to 60 days after the cessation.
|
| 409 |
+
|
| 410 |
+
Moreover, your license from a particular copyright holder is
|
| 411 |
+
reinstated permanently if the copyright holder notifies you of the
|
| 412 |
+
violation by some reasonable means, this is the first time you have
|
| 413 |
+
received notice of violation of this License (for any work) from that
|
| 414 |
+
copyright holder, and you cure the violation prior to 30 days after
|
| 415 |
+
your receipt of the notice.
|
| 416 |
+
|
| 417 |
+
Termination of your rights under this section does not terminate the
|
| 418 |
+
licenses of parties who have received copies or rights from you under
|
| 419 |
+
this License. If your rights have been terminated and not permanently
|
| 420 |
+
reinstated, you do not qualify to receive new licenses for the same
|
| 421 |
+
material under section 10.
|
| 422 |
+
|
| 423 |
+
9. Acceptance Not Required for Having Copies.
|
| 424 |
+
|
| 425 |
+
You are not required to accept this License in order to receive or
|
| 426 |
+
run a copy of the Program. Ancillary propagation of a covered work
|
| 427 |
+
occurring solely as a consequence of using peer-to-peer transmission
|
| 428 |
+
to receive a copy likewise does not require acceptance. However,
|
| 429 |
+
nothing other than this License grants you permission to propagate or
|
| 430 |
+
modify any covered work. These actions infringe copyright if you do
|
| 431 |
+
not accept this License. Therefore, by modifying or propagating a
|
| 432 |
+
covered work, you indicate your acceptance of this License to do so.
|
| 433 |
+
|
| 434 |
+
10. Automatic Licensing of Downstream Recipients.
|
| 435 |
+
|
| 436 |
+
Each time you convey a covered work, the recipient automatically
|
| 437 |
+
receives a license from the original licensors, to run, modify and
|
| 438 |
+
propagate that work, subject to this License. You are not responsible
|
| 439 |
+
for enforcing compliance by third parties with this License.
|
| 440 |
+
|
| 441 |
+
An "entity transaction" is a transaction transferring control of an
|
| 442 |
+
organization, or substantially all assets of one, or subdividing an
|
| 443 |
+
organization, or merging organizations. If propagation of a covered
|
| 444 |
+
work results from an entity transaction, each party to that
|
| 445 |
+
transaction who receives a copy of the work also receives whatever
|
| 446 |
+
licenses to the work the party's predecessor in interest had or could
|
| 447 |
+
give under the previous paragraph, plus a right to possession of the
|
| 448 |
+
Corresponding Source of the work from the predecessor in interest, if
|
| 449 |
+
the predecessor has it or can get it with reasonable efforts.
|
| 450 |
+
|
| 451 |
+
You may not impose any further restrictions on the exercise of the
|
| 452 |
+
rights granted or affirmed under this License. For example, you may
|
| 453 |
+
not impose a license fee, royalty, or other charge for exercise of
|
| 454 |
+
rights granted under this License, and you may not initiate litigation
|
| 455 |
+
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
| 456 |
+
any patent claim is infringed by making, using, selling, offering for
|
| 457 |
+
sale, or importing the Program or any portion of it.
|
| 458 |
+
|
| 459 |
+
11. Patents.
|
| 460 |
+
|
| 461 |
+
A "contributor" is a copyright holder who authorizes use under this
|
| 462 |
+
License of the Program or a work on which the Program is based. The
|
| 463 |
+
work thus licensed is called the contributor's "contributor version".
|
| 464 |
+
|
| 465 |
+
A contributor's "essential patent claims" are all patent claims
|
| 466 |
+
owned or controlled by the contributor, whether already acquired or
|
| 467 |
+
hereafter acquired, that would be infringed by some manner, permitted
|
| 468 |
+
by this License, of making, using, or selling its contributor version,
|
| 469 |
+
but do not include claims that would be infringed only as a
|
| 470 |
+
consequence of further modification of the contributor version. For
|
| 471 |
+
purposes of this definition, "control" includes the right to grant
|
| 472 |
+
patent sublicenses in a manner consistent with the requirements of
|
| 473 |
+
this License.
|
| 474 |
+
|
| 475 |
+
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
| 476 |
+
patent license under the contributor's essential patent claims, to
|
| 477 |
+
make, use, sell, offer for sale, import and otherwise run, modify and
|
| 478 |
+
propagate the contents of its contributor version.
|
| 479 |
+
|
| 480 |
+
In the following three paragraphs, a "patent license" is any express
|
| 481 |
+
agreement or commitment, however denominated, not to enforce a patent
|
| 482 |
+
(such as an express permission to practice a patent or covenant not to
|
| 483 |
+
sue for patent infringement). To "grant" such a patent license to a
|
| 484 |
+
party means to make such an agreement or commitment not to enforce a
|
| 485 |
+
patent against the party.
|
| 486 |
+
|
| 487 |
+
If you convey a covered work, knowingly relying on a patent license,
|
| 488 |
+
and the Corresponding Source of the work is not available for anyone
|
| 489 |
+
to copy, free of charge and under the terms of this License, through a
|
| 490 |
+
publicly available network server or other readily accessible means,
|
| 491 |
+
then you must either (1) cause the Corresponding Source to be so
|
| 492 |
+
available, or (2) arrange to deprive yourself of the benefit of the
|
| 493 |
+
patent license for this particular work, or (3) arrange, in a manner
|
| 494 |
+
consistent with the requirements of this License, to extend the patent
|
| 495 |
+
license to downstream recipients. "Knowingly relying" means you have
|
| 496 |
+
actual knowledge that, but for the patent license, your conveying the
|
| 497 |
+
covered work in a country, or your recipient's use of the covered work
|
| 498 |
+
in a country, would infringe one or more identifiable patents in that
|
| 499 |
+
country that you have reason to believe are valid.
|
| 500 |
+
|
| 501 |
+
If, pursuant to or in connection with a single transaction or
|
| 502 |
+
arrangement, you convey, or propagate by procuring conveyance of, a
|
| 503 |
+
covered work, and grant a patent license to some of the parties
|
| 504 |
+
receiving the covered work authorizing them to use, propagate, modify
|
| 505 |
+
or convey a specific copy of the covered work, then the patent license
|
| 506 |
+
you grant is automatically extended to all recipients of the covered
|
| 507 |
+
work and works based on it.
|
| 508 |
+
|
| 509 |
+
A patent license is "discriminatory" if it does not include within
|
| 510 |
+
the scope of its coverage, prohibits the exercise of, or is
|
| 511 |
+
conditioned on the non-exercise of one or more of the rights that are
|
| 512 |
+
specifically granted under this License. You may not convey a covered
|
| 513 |
+
work if you are a party to an arrangement with a third party that is
|
| 514 |
+
in the business of distributing software, under which you make payment
|
| 515 |
+
to the third party based on the extent of your activity of conveying
|
| 516 |
+
the work, and under which the third party grants, to any of the
|
| 517 |
+
parties who would receive the covered work from you, a discriminatory
|
| 518 |
+
patent license (a) in connection with copies of the covered work
|
| 519 |
+
conveyed by you (or copies made from those copies), or (b) primarily
|
| 520 |
+
for and in connection with specific products or compilations that
|
| 521 |
+
contain the covered work, unless you entered into that arrangement,
|
| 522 |
+
or that patent license was granted, prior to 28 March 2007.
|
| 523 |
+
|
| 524 |
+
Nothing in this License shall be construed as excluding or limiting
|
| 525 |
+
any implied license or other defenses to infringement that may
|
| 526 |
+
otherwise be available to you under applicable patent law.
|
| 527 |
+
|
| 528 |
+
12. No Surrender of Others' Freedom.
|
| 529 |
+
|
| 530 |
+
If conditions are imposed on you (whether by court order, agreement or
|
| 531 |
+
otherwise) that contradict the conditions of this License, they do not
|
| 532 |
+
excuse you from the conditions of this License. If you cannot convey a
|
| 533 |
+
covered work so as to satisfy simultaneously your obligations under this
|
| 534 |
+
License and any other pertinent obligations, then as a consequence you may
|
| 535 |
+
not convey it at all. For example, if you agree to terms that obligate you
|
| 536 |
+
to collect a royalty for further conveying from those to whom you convey
|
| 537 |
+
the Program, the only way you could satisfy both those terms and this
|
| 538 |
+
License would be to refrain entirely from conveying the Program.
|
| 539 |
+
|
| 540 |
+
13. Remote Network Interaction; Use with the GNU General Public License.
|
| 541 |
+
|
| 542 |
+
Notwithstanding any other provision of this License, if you modify the
|
| 543 |
+
Program, your modified version must prominently offer all users
|
| 544 |
+
interacting with it remotely through a computer network (if your version
|
| 545 |
+
supports such interaction) an opportunity to receive the Corresponding
|
| 546 |
+
Source of your version by providing access to the Corresponding Source
|
| 547 |
+
from a network server at no charge, through some standard or customary
|
| 548 |
+
means of facilitating copying of software. This Corresponding Source
|
| 549 |
+
shall include the Corresponding Source for any work covered by version 3
|
| 550 |
+
of the GNU General Public License that is incorporated pursuant to the
|
| 551 |
+
following paragraph.
|
| 552 |
+
|
| 553 |
+
Notwithstanding any other provision of this License, you have
|
| 554 |
+
permission to link or combine any covered work with a work licensed
|
| 555 |
+
under version 3 of the GNU General Public License into a single
|
| 556 |
+
combined work, and to convey the resulting work. The terms of this
|
| 557 |
+
License will continue to apply to the part which is the covered work,
|
| 558 |
+
but the work with which it is combined will remain governed by version
|
| 559 |
+
3 of the GNU General Public License.
|
| 560 |
+
|
| 561 |
+
14. Revised Versions of this License.
|
| 562 |
+
|
| 563 |
+
The Free Software Foundation may publish revised and/or new versions of
|
| 564 |
+
the GNU Affero General Public License from time to time. Such new versions
|
| 565 |
+
will be similar in spirit to the present version, but may differ in detail to
|
| 566 |
+
address new problems or concerns.
|
| 567 |
+
|
| 568 |
+
Each version is given a distinguishing version number. If the
|
| 569 |
+
Program specifies that a certain numbered version of the GNU Affero General
|
| 570 |
+
Public License "or any later version" applies to it, you have the
|
| 571 |
+
option of following the terms and conditions either of that numbered
|
| 572 |
+
version or of any later version published by the Free Software
|
| 573 |
+
Foundation. If the Program does not specify a version number of the
|
| 574 |
+
GNU Affero General Public License, you may choose any version ever published
|
| 575 |
+
by the Free Software Foundation.
|
| 576 |
+
|
| 577 |
+
If the Program specifies that a proxy can decide which future
|
| 578 |
+
versions of the GNU Affero General Public License can be used, that proxy's
|
| 579 |
+
public statement of acceptance of a version permanently authorizes you
|
| 580 |
+
to choose that version for the Program.
|
| 581 |
+
|
| 582 |
+
Later license versions may give you additional or different
|
| 583 |
+
permissions. However, no additional obligations are imposed on any
|
| 584 |
+
author or copyright holder as a result of your choosing to follow a
|
| 585 |
+
later version.
|
| 586 |
+
|
| 587 |
+
15. Disclaimer of Warranty.
|
| 588 |
+
|
| 589 |
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
| 590 |
+
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
| 591 |
+
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
| 592 |
+
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
| 593 |
+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
| 594 |
+
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
| 595 |
+
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
| 596 |
+
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
| 597 |
+
|
| 598 |
+
16. Limitation of Liability.
|
| 599 |
+
|
| 600 |
+
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
| 601 |
+
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
| 602 |
+
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
| 603 |
+
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
| 604 |
+
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
| 605 |
+
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
| 606 |
+
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
| 607 |
+
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
| 608 |
+
SUCH DAMAGES.
|
| 609 |
+
|
| 610 |
+
17. Interpretation of Sections 15 and 16.
|
| 611 |
+
|
| 612 |
+
If the disclaimer of warranty and limitation of liability provided
|
| 613 |
+
above cannot be given local legal effect according to their terms,
|
| 614 |
+
reviewing courts shall apply local law that most closely approximates
|
| 615 |
+
an absolute waiver of all civil liability in connection with the
|
| 616 |
+
Program, unless a warranty or assumption of liability accompanies a
|
| 617 |
+
copy of the Program in return for a fee.
|
| 618 |
+
|
| 619 |
+
END OF TERMS AND CONDITIONS
|
| 620 |
+
|
| 621 |
+
How to Apply These Terms to Your New Programs
|
| 622 |
+
|
| 623 |
+
If you develop a new program, and you want it to be of the greatest
|
| 624 |
+
possible use to the public, the best way to achieve this is to make it
|
| 625 |
+
free software which everyone can redistribute and change under these terms.
|
| 626 |
+
|
| 627 |
+
To do so, attach the following notices to the program. It is safest
|
| 628 |
+
to attach them to the start of each source file to most effectively
|
| 629 |
+
state the exclusion of warranty; and each file should have at least
|
| 630 |
+
the "copyright" line and a pointer to where the full notice is found.
|
| 631 |
+
|
| 632 |
+
<one line to give the program's name and a brief idea of what it does.>
|
| 633 |
+
Copyright (C) <year> <name of author>
|
| 634 |
+
|
| 635 |
+
This program is free software: you can redistribute it and/or modify
|
| 636 |
+
it under the terms of the GNU Affero General Public License as published by
|
| 637 |
+
the Free Software Foundation, either version 3 of the License, or
|
| 638 |
+
(at your option) any later version.
|
| 639 |
+
|
| 640 |
+
This program is distributed in the hope that it will be useful,
|
| 641 |
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 642 |
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 643 |
+
GNU Affero General Public License for more details.
|
| 644 |
+
|
| 645 |
+
You should have received a copy of the GNU Affero General Public License
|
| 646 |
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 647 |
+
|
| 648 |
+
Also add information on how to contact you by electronic and paper mail.
|
| 649 |
+
|
| 650 |
+
If your software can interact with users remotely through a computer
|
| 651 |
+
network, you should also make sure that it provides a way for users to
|
| 652 |
+
get its source. For example, if your program is a web application, its
|
| 653 |
+
interface could display a "Source" link that leads users to an archive
|
| 654 |
+
of the code. There are many ways you could offer source, and different
|
| 655 |
+
solutions will be better for different programs; see section 13 for the
|
| 656 |
+
specific requirements.
|
| 657 |
+
|
| 658 |
+
You should also get your employer (if you work as a programmer) or school,
|
| 659 |
+
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
| 660 |
+
For more information on this, and how to apply and follow the GNU AGPL, see
|
| 661 |
+
<https://www.gnu.org/licenses/>.
|
README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: agpl-3.0
|
| 3 |
+
tags:
|
| 4 |
+
- snapkitty
|
| 5 |
+
- october-2026-drop
|
| 6 |
+
- j
|
| 7 |
+
- prolog
|
| 8 |
+
- postgresql
|
| 9 |
+
- pgvector
|
| 10 |
+
- python
|
| 11 |
+
- mixture-of-agents
|
| 12 |
+
- router
|
| 13 |
+
- agents
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
> Mirrored from [https://github.com/SNAPKITTYWEST/david](https://github.com/SNAPKITTYWEST/david) at commit `4ecbee9`. Part of the **SnapKitty October 2026 main drop**.
|
| 17 |
+
|
docs/contracts.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# David contracts
|
| 2 |
+
|
| 3 |
+
## WorkItem (agent input)
|
| 4 |
+
|
| 5 |
+
```
|
| 6 |
+
request_id, work_id, agent_id
|
| 7 |
+
task, evidence_bundle, required_outputs
|
| 8 |
+
permissions, deadline, risk_policy
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
## AgentResult (agent output)
|
| 12 |
+
|
| 13 |
+
```
|
| 14 |
+
agent_id
|
| 15 |
+
facts[], conclusions[], assumptions[], uncertainty[]
|
| 16 |
+
evidence_refs[], transformations[]
|
| 17 |
+
risk_signals[], provenance_root, status
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
## Evidence / EvidenceBundle
|
| 21 |
+
|
| 22 |
+
See architecture Β§5. Every chunk carries source, hash, authority, and parent links β
|
| 23 |
+
agents never receive bare text without provenance.
|
| 24 |
+
|
| 25 |
+
## Execution wiring
|
| 26 |
+
|
| 27 |
+
`execution/` builds one WorkItem per selected `agent_id` and dispatches only that set.
|
| 28 |
+
Unselected agents (e.g. Quant on a COBOL migration query) never receive work.
|
| 29 |
+
|
| 30 |
+
## Symbolic hard filters (`router/symbolic.py`)
|
| 31 |
+
|
| 32 |
+
Predicates (not score penalties). An agent is eligible only if all three hold:
|
| 33 |
+
|
| 34 |
+
- **capability_eligible** β `capabilities β© required_capabilities β β
`, or `control_plane`
|
| 35 |
+
- **permission_eligible** β `permissions β granted_permissions`
|
| 36 |
+
- **risk_eligible** β `risk_class` does not exceed `max_risk_class` (`low < medium < high < critical`)
|
| 37 |
+
|
| 38 |
+
`apply_symbolic_filters` returns the agents that pass all three.
|
| 39 |
+
`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`).
|
| 40 |
+
|
| 41 |
+
## J execution (product)
|
| 42 |
+
|
| 43 |
+
`j/execution.ijs` consumes the Prolog selected-set (one `agent_id` per line) and builds
|
| 44 |
+
WorkItems only for that set. `bundle_ok` requires each evidence row to carry non-empty
|
| 45 |
+
`evidence_id`, `source_id`, and `content_hash` β bare text is rejected.
|
docs/images/.gitkeep
ADDED
|
File without changes
|
docs/images/david-demos.png
ADDED
|
Git LFS Details
|
docs/images/david-evidence.png
ADDED
|
Git LFS Details
|
docs/images/david-execution.png
ADDED
|
Git LFS Details
|
docs/images/david-prolog.png
ADDED
|
Git LFS Details
|
evidence/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""In-memory evidence retrieval (mirrors sql/001 hybrid sketch; no live DB)."""
|
| 2 |
+
|
| 3 |
+
from .retrieve import (
|
| 4 |
+
EvidenceRecord,
|
| 5 |
+
EvidenceIndex,
|
| 6 |
+
retrieve,
|
| 7 |
+
seed_cobol_corpus,
|
| 8 |
+
empty_bundle,
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
"EvidenceRecord",
|
| 13 |
+
"EvidenceIndex",
|
| 14 |
+
"retrieve",
|
| 15 |
+
"seed_cobol_corpus",
|
| 16 |
+
"empty_bundle",
|
| 17 |
+
]
|
evidence/retrieve.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hybrid evidence retrieval β dense + lexical + metadata + provenance.
|
| 2 |
+
|
| 3 |
+
In-memory stand-in for sql/001_agent_and_evidence_indexes.sql.
|
| 4 |
+
Production SQL uses vector(1536); tests use fixed dim-8 vectors (no live DB).
|
| 5 |
+
|
| 6 |
+
Merge / rerank rule (deterministic):
|
| 7 |
+
score = dense_cosine + lexical_overlap + source_authority - penalties
|
| 8 |
+
sort by (-score, evidence_id)
|
| 9 |
+
penalties: missing required parent_evidence when policy requires it
|
| 10 |
+
|
| 11 |
+
Hybrid note: a chunk with weak dense but strong lexical can still rank
|
| 12 |
+
if lexical_overlap compensates (no hard dense floor).
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import hashlib
|
| 18 |
+
import math
|
| 19 |
+
import re
|
| 20 |
+
from dataclasses import dataclass, field
|
| 21 |
+
from typing import Any, Iterable, Sequence
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Production pgvector column is vector(1536); test corpus uses tiny fixed vectors.
|
| 25 |
+
EMBEDDING_DIM_TEST = 8
|
| 26 |
+
EMBEDDING_DIM_PRODUCTION = 1536
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@dataclass
|
| 30 |
+
class EvidenceRecord:
|
| 31 |
+
"""Fields mirror the SQL ``evidence`` table."""
|
| 32 |
+
|
| 33 |
+
evidence_id: str
|
| 34 |
+
source_id: str
|
| 35 |
+
source_type: str
|
| 36 |
+
document_id: str | None
|
| 37 |
+
location: str | None
|
| 38 |
+
content: str
|
| 39 |
+
content_hash: str
|
| 40 |
+
embedding: list[float] | None
|
| 41 |
+
source_authority: float = 0.5
|
| 42 |
+
parent_evidence: tuple[str, ...] = ()
|
| 43 |
+
created_at: str | None = None
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _content_hash(text: str) -> str:
|
| 47 |
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _tokenize(text: str) -> set[str]:
|
| 51 |
+
return {t for t in re.findall(r"[a-z0-9]+", text.lower()) if len(t) > 1}
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def cosine_similarity(a: Sequence[float], b: Sequence[float]) -> float:
|
| 55 |
+
if not a or not b or len(a) != len(b):
|
| 56 |
+
return 0.0
|
| 57 |
+
dot = sum(x * y for x, y in zip(a, b))
|
| 58 |
+
na = math.sqrt(sum(x * x for x in a))
|
| 59 |
+
nb = math.sqrt(sum(y * y for y in b))
|
| 60 |
+
if na == 0.0 or nb == 0.0:
|
| 61 |
+
return 0.0
|
| 62 |
+
return dot / (na * nb)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def lexical_score(query: str, content: str) -> float:
|
| 66 |
+
"""Token overlap stand-in for ``content_tsv @@ plainto_tsquery``."""
|
| 67 |
+
q = _tokenize(query)
|
| 68 |
+
if not q:
|
| 69 |
+
return 0.0
|
| 70 |
+
c = _tokenize(content)
|
| 71 |
+
if not c:
|
| 72 |
+
return 0.0
|
| 73 |
+
return len(q & c) / len(q)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def empty_bundle() -> dict[str, Any]:
|
| 77 |
+
"""EvidenceBundle-shaped dict with no hits (contracts.md)."""
|
| 78 |
+
return {
|
| 79 |
+
"facts": [],
|
| 80 |
+
"sources": [],
|
| 81 |
+
"provenance": [],
|
| 82 |
+
"retrieval_metadata": [],
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def seed_cobol_corpus() -> list[EvidenceRecord]:
|
| 87 |
+
"""Fixed COBOL-modernization corpus (dim-8 embeddings for tests).
|
| 88 |
+
|
| 89 |
+
Axes (approx): [cobol, balance, payment, schema, pricing, vol, greek, junk]
|
| 90 |
+
"""
|
| 91 |
+
# Strong COBOL / balance / payment cluster
|
| 92 |
+
copybook = EvidenceRecord(
|
| 93 |
+
evidence_id="ev_copybook_01",
|
| 94 |
+
source_id="src_copybook_acct",
|
| 95 |
+
source_type="copybook",
|
| 96 |
+
document_id="ACCTREC.cpy",
|
| 97 |
+
location="01-ACCOUNT-BALANCE",
|
| 98 |
+
content=(
|
| 99 |
+
"01 ACCOUNT-RECORD. "
|
| 100 |
+
"05 ACCOUNT-BALANCE PIC S9(9)V99. "
|
| 101 |
+
"05 PAYMENT-AMOUNT PIC S9(7)V99. "
|
| 102 |
+
"05 LAST-PAYMENT-DATE PIC X(8)."
|
| 103 |
+
),
|
| 104 |
+
content_hash="",
|
| 105 |
+
embedding=[0.95, 0.90, 0.85, 0.20, 0.05, 0.0, 0.0, 0.0],
|
| 106 |
+
source_authority=0.95,
|
| 107 |
+
parent_evidence=("ev_provenance_root",),
|
| 108 |
+
created_at="2026-01-01T00:00:00Z",
|
| 109 |
+
)
|
| 110 |
+
cobol_src = EvidenceRecord(
|
| 111 |
+
evidence_id="ev_cobol_src_01",
|
| 112 |
+
source_id="src_cobol_pay",
|
| 113 |
+
source_type="cobol_source",
|
| 114 |
+
document_id="PAYROLL.cbl",
|
| 115 |
+
location="PROCEDURE DIVISION / CALC-BALANCE",
|
| 116 |
+
content=(
|
| 117 |
+
"COMPUTE ACCOUNT-BALANCE = ACCOUNT-BALANCE - PAYMENT-AMOUNT. "
|
| 118 |
+
"IF ACCOUNT-BALANCE < ZERO THEN PERFORM OVERDRAFT-CHECK. "
|
| 119 |
+
"MOVE PAYMENT-AMOUNT TO WS-LAST-PAYMENT."
|
| 120 |
+
),
|
| 121 |
+
content_hash="",
|
| 122 |
+
embedding=[0.92, 0.88, 0.90, 0.15, 0.05, 0.0, 0.0, 0.0],
|
| 123 |
+
source_authority=0.90,
|
| 124 |
+
parent_evidence=("ev_provenance_root",),
|
| 125 |
+
created_at="2026-01-01T00:00:00Z",
|
| 126 |
+
)
|
| 127 |
+
db2 = EvidenceRecord(
|
| 128 |
+
evidence_id="ev_db2_schema_01",
|
| 129 |
+
source_id="src_db2_acct",
|
| 130 |
+
source_type="db2_schema",
|
| 131 |
+
document_id="ACCT.DDL",
|
| 132 |
+
location="TABLE ACCOUNT_BALANCES",
|
| 133 |
+
content=(
|
| 134 |
+
"CREATE TABLE ACCOUNT_BALANCES ("
|
| 135 |
+
"ACCT_ID CHAR(12), BALANCE DECIMAL(11,2), "
|
| 136 |
+
"LAST_PAYMENT DECIMAL(9,2), UPDATED_TS TIMESTAMP)."
|
| 137 |
+
),
|
| 138 |
+
content_hash="",
|
| 139 |
+
embedding=[0.70, 0.80, 0.55, 0.95, 0.05, 0.0, 0.0, 0.0],
|
| 140 |
+
source_authority=0.85,
|
| 141 |
+
parent_evidence=("ev_provenance_root",),
|
| 142 |
+
created_at="2026-01-01T00:00:00Z",
|
| 143 |
+
)
|
| 144 |
+
# Should NOT win on a COBOL balance query (pricing / quant cluster)
|
| 145 |
+
quant = EvidenceRecord(
|
| 146 |
+
evidence_id="ev_quant_pricing_01",
|
| 147 |
+
source_id="src_quant_report",
|
| 148 |
+
source_type="quant_report",
|
| 149 |
+
document_id="PRICING_Q3.pdf",
|
| 150 |
+
location="section:monte_carlo",
|
| 151 |
+
content=(
|
| 152 |
+
"Monte Carlo pricing of exotic options. "
|
| 153 |
+
"Implied volatility surface and greek sensitivities. "
|
| 154 |
+
"No COBOL or account balance fields."
|
| 155 |
+
),
|
| 156 |
+
content_hash="",
|
| 157 |
+
embedding=[0.05, 0.05, 0.05, 0.05, 0.95, 0.90, 0.85, 0.10],
|
| 158 |
+
source_authority=0.80,
|
| 159 |
+
parent_evidence=("ev_provenance_root",),
|
| 160 |
+
created_at="2026-01-01T00:00:00Z",
|
| 161 |
+
)
|
| 162 |
+
# Low-authority junk for provenance filter tests
|
| 163 |
+
junk = EvidenceRecord(
|
| 164 |
+
evidence_id="ev_junk_lowauth_01",
|
| 165 |
+
source_id="src_web_scrape",
|
| 166 |
+
source_type="web_scrape",
|
| 167 |
+
document_id="random-blog.html",
|
| 168 |
+
location="body",
|
| 169 |
+
content=(
|
| 170 |
+
"Someone said COBOL payment balance might work like a spreadsheet. "
|
| 171 |
+
"Unverified forum post."
|
| 172 |
+
),
|
| 173 |
+
content_hash="",
|
| 174 |
+
embedding=[0.60, 0.55, 0.50, 0.10, 0.10, 0.0, 0.0, 0.9],
|
| 175 |
+
source_authority=0.15,
|
| 176 |
+
parent_evidence=(),
|
| 177 |
+
created_at="2026-01-01T00:00:00Z",
|
| 178 |
+
)
|
| 179 |
+
# Weak dense (far from COBOL cluster) but strong lexical on balance/payment/cobol
|
| 180 |
+
# β hybrid: lexical can lift it into results when dense alone would not.
|
| 181 |
+
lexical_lift = EvidenceRecord(
|
| 182 |
+
evidence_id="ev_lexical_lift_01",
|
| 183 |
+
source_id="src_glossary",
|
| 184 |
+
source_type="glossary",
|
| 185 |
+
document_id="LEGACY-GLOSSARY.txt",
|
| 186 |
+
location="entry:PAYMENT-BALANCE",
|
| 187 |
+
content=(
|
| 188 |
+
"cobol payment balance glossary entry: "
|
| 189 |
+
"PAYMENT and BALANCE fields on ACCOUNT records in legacy batch."
|
| 190 |
+
),
|
| 191 |
+
content_hash="",
|
| 192 |
+
# Oriented away from COBOL cluster β weak cosine vs COBOL query
|
| 193 |
+
embedding=[0.10, 0.15, 0.10, 0.05, 0.40, 0.35, 0.30, 0.20],
|
| 194 |
+
source_authority=0.70,
|
| 195 |
+
parent_evidence=("ev_provenance_root",),
|
| 196 |
+
created_at="2026-01-01T00:00:00Z",
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
records = [copybook, cobol_src, db2, quant, junk, lexical_lift]
|
| 200 |
+
for r in records:
|
| 201 |
+
r.content_hash = _content_hash(r.content)
|
| 202 |
+
return records
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
# Canonical COBOL payment/balance query embedding (dim 8)
|
| 206 |
+
COBOL_BALANCE_QUERY = "cobol account payment balance CALC-BALANCE"
|
| 207 |
+
COBOL_BALANCE_QUERY_VEC = [0.93, 0.91, 0.88, 0.25, 0.05, 0.0, 0.0, 0.0]
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
@dataclass
|
| 211 |
+
class EvidenceIndex:
|
| 212 |
+
"""In-memory evidence index (no Postgres required)."""
|
| 213 |
+
|
| 214 |
+
records: list[EvidenceRecord] = field(default_factory=list)
|
| 215 |
+
|
| 216 |
+
def __post_init__(self) -> None:
|
| 217 |
+
if not self.records:
|
| 218 |
+
self.records = seed_cobol_corpus()
|
| 219 |
+
self._by_id = {r.evidence_id: r for r in self.records}
|
| 220 |
+
|
| 221 |
+
def add(self, record: EvidenceRecord) -> None:
|
| 222 |
+
if not record.content_hash:
|
| 223 |
+
record.content_hash = _content_hash(record.content)
|
| 224 |
+
self.records.append(record)
|
| 225 |
+
self._by_id[record.evidence_id] = record
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def retrieve(
|
| 229 |
+
query: str,
|
| 230 |
+
*,
|
| 231 |
+
query_embedding: Sequence[float] | None = None,
|
| 232 |
+
index: EvidenceIndex | None = None,
|
| 233 |
+
source_types: Iterable[str] | None = None,
|
| 234 |
+
document_ids: Iterable[str] | None = None,
|
| 235 |
+
min_authority: float = 0.0,
|
| 236 |
+
require_parent_evidence: bool = False,
|
| 237 |
+
top_k: int = 10,
|
| 238 |
+
) -> dict[str, Any]:
|
| 239 |
+
"""Hybrid retrieve β EvidenceBundle-shaped dict (facts/sources/provenance/β¦).
|
| 240 |
+
|
| 241 |
+
Agents never receive bare text without provenance fields on each source.
|
| 242 |
+
"""
|
| 243 |
+
if not query or not query.strip():
|
| 244 |
+
return empty_bundle()
|
| 245 |
+
|
| 246 |
+
idx = index or EvidenceIndex()
|
| 247 |
+
st_filter = set(source_types) if source_types is not None else None
|
| 248 |
+
doc_filter = set(document_ids) if document_ids is not None else None
|
| 249 |
+
q_emb = list(query_embedding) if query_embedding is not None else None
|
| 250 |
+
|
| 251 |
+
scored: list[tuple[float, EvidenceRecord, dict[str, float]]] = []
|
| 252 |
+
for rec in idx.records:
|
| 253 |
+
# Metadata filters
|
| 254 |
+
if st_filter is not None and rec.source_type not in st_filter:
|
| 255 |
+
continue
|
| 256 |
+
if doc_filter is not None and rec.document_id not in doc_filter:
|
| 257 |
+
continue
|
| 258 |
+
# Provenance / authority
|
| 259 |
+
if rec.source_authority < min_authority:
|
| 260 |
+
continue
|
| 261 |
+
if require_parent_evidence and not rec.parent_evidence:
|
| 262 |
+
continue
|
| 263 |
+
|
| 264 |
+
dense = 0.0
|
| 265 |
+
if q_emb is not None and rec.embedding is not None:
|
| 266 |
+
dense = cosine_similarity(q_emb, rec.embedding)
|
| 267 |
+
lex = lexical_score(query, rec.content)
|
| 268 |
+
authority = float(rec.source_authority)
|
| 269 |
+
penalty = 0.0
|
| 270 |
+
if require_parent_evidence and not rec.parent_evidence:
|
| 271 |
+
penalty += 1.0 # defensive; already filtered above
|
| 272 |
+
|
| 273 |
+
score = dense + lex + authority - penalty
|
| 274 |
+
parts = {
|
| 275 |
+
"dense": dense,
|
| 276 |
+
"lexical": lex,
|
| 277 |
+
"source_authority": authority,
|
| 278 |
+
"penalty": penalty,
|
| 279 |
+
"score": score,
|
| 280 |
+
}
|
| 281 |
+
scored.append((score, rec, parts))
|
| 282 |
+
|
| 283 |
+
# Deterministic rerank: score desc, then evidence_id asc
|
| 284 |
+
scored.sort(key=lambda t: (-t[0], t[1].evidence_id))
|
| 285 |
+
top = scored[:top_k]
|
| 286 |
+
|
| 287 |
+
if not top:
|
| 288 |
+
return empty_bundle()
|
| 289 |
+
|
| 290 |
+
facts: list[dict[str, Any]] = []
|
| 291 |
+
sources: list[dict[str, Any]] = []
|
| 292 |
+
provenance: list[dict[str, Any]] = []
|
| 293 |
+
retrieval_metadata: list[dict[str, Any]] = []
|
| 294 |
+
|
| 295 |
+
for score, rec, parts in top:
|
| 296 |
+
facts.append(
|
| 297 |
+
{
|
| 298 |
+
"evidence_id": rec.evidence_id,
|
| 299 |
+
"summary": rec.content[:160],
|
| 300 |
+
"source_type": rec.source_type,
|
| 301 |
+
}
|
| 302 |
+
)
|
| 303 |
+
sources.append(
|
| 304 |
+
{
|
| 305 |
+
"evidence_id": rec.evidence_id,
|
| 306 |
+
"source_id": rec.source_id,
|
| 307 |
+
"source_type": rec.source_type,
|
| 308 |
+
"document_id": rec.document_id,
|
| 309 |
+
"location": rec.location,
|
| 310 |
+
"content_hash": rec.content_hash,
|
| 311 |
+
"source_authority": rec.source_authority,
|
| 312 |
+
"parent_evidence": list(rec.parent_evidence),
|
| 313 |
+
"content": rec.content,
|
| 314 |
+
"retrieval_score": score,
|
| 315 |
+
}
|
| 316 |
+
)
|
| 317 |
+
provenance.append(
|
| 318 |
+
{
|
| 319 |
+
"evidence_id": rec.evidence_id,
|
| 320 |
+
"source_id": rec.source_id,
|
| 321 |
+
"content_hash": rec.content_hash,
|
| 322 |
+
"parent_evidence": list(rec.parent_evidence),
|
| 323 |
+
"source_authority": rec.source_authority,
|
| 324 |
+
}
|
| 325 |
+
)
|
| 326 |
+
retrieval_metadata.append(
|
| 327 |
+
{
|
| 328 |
+
"evidence_id": rec.evidence_id,
|
| 329 |
+
"dense": parts["dense"],
|
| 330 |
+
"lexical": parts["lexical"],
|
| 331 |
+
"source_authority": parts["source_authority"],
|
| 332 |
+
"penalty": parts["penalty"],
|
| 333 |
+
"score": parts["score"],
|
| 334 |
+
"embedding_dim_note": (
|
| 335 |
+
f"test vectors dim={EMBEDDING_DIM_TEST}; "
|
| 336 |
+
f"production SQL uses vector({EMBEDDING_DIM_PRODUCTION})"
|
| 337 |
+
),
|
| 338 |
+
}
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
return {
|
| 342 |
+
"facts": facts,
|
| 343 |
+
"sources": sources,
|
| 344 |
+
"provenance": provenance,
|
| 345 |
+
"retrieval_metadata": retrieval_metadata,
|
| 346 |
+
}
|
evidence/test_retrieve.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for in-memory hybrid evidence retrieval."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import unittest
|
| 6 |
+
|
| 7 |
+
from evidence.retrieve import (
|
| 8 |
+
COBOL_BALANCE_QUERY,
|
| 9 |
+
COBOL_BALANCE_QUERY_VEC,
|
| 10 |
+
EvidenceIndex,
|
| 11 |
+
empty_bundle,
|
| 12 |
+
lexical_score,
|
| 13 |
+
retrieve,
|
| 14 |
+
seed_cobol_corpus,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class RetrieveTests(unittest.TestCase):
|
| 19 |
+
def setUp(self):
|
| 20 |
+
self.index = EvidenceIndex(seed_cobol_corpus())
|
| 21 |
+
self.query = COBOL_BALANCE_QUERY
|
| 22 |
+
self.qvec = COBOL_BALANCE_QUERY_VEC
|
| 23 |
+
|
| 24 |
+
def test_cobol_balance_prefers_legacy_not_quant(self):
|
| 25 |
+
bundle = retrieve(
|
| 26 |
+
self.query,
|
| 27 |
+
query_embedding=self.qvec,
|
| 28 |
+
index=self.index,
|
| 29 |
+
min_authority=0.5,
|
| 30 |
+
require_parent_evidence=True,
|
| 31 |
+
top_k=5,
|
| 32 |
+
)
|
| 33 |
+
ids = [s["evidence_id"] for s in bundle["sources"]]
|
| 34 |
+
self.assertTrue(ids, "expected non-empty sources")
|
| 35 |
+
# Top hit must not be the quant pricing chunk
|
| 36 |
+
self.assertNotEqual(ids[0], "ev_quant_pricing_01")
|
| 37 |
+
# COBOL / copybook / schema evidence should appear
|
| 38 |
+
self.assertTrue(
|
| 39 |
+
any(
|
| 40 |
+
i in ids
|
| 41 |
+
for i in ("ev_copybook_01", "ev_cobol_src_01", "ev_db2_schema_01")
|
| 42 |
+
),
|
| 43 |
+
f"expected legacy evidence in {ids}",
|
| 44 |
+
)
|
| 45 |
+
# Quant must not be the top hit; preferably absent from top-3 when
|
| 46 |
+
# authority + dense favor legacy chunks
|
| 47 |
+
top3 = ids[:3]
|
| 48 |
+
self.assertNotIn("ev_quant_pricing_01", top3)
|
| 49 |
+
|
| 50 |
+
def test_provenance_authority_drops_junk(self):
|
| 51 |
+
# Without filter, junk may score on lexical overlap
|
| 52 |
+
open_bundle = retrieve(
|
| 53 |
+
self.query,
|
| 54 |
+
query_embedding=self.qvec,
|
| 55 |
+
index=self.index,
|
| 56 |
+
min_authority=0.0,
|
| 57 |
+
require_parent_evidence=False,
|
| 58 |
+
top_k=10,
|
| 59 |
+
)
|
| 60 |
+
open_ids = {s["evidence_id"] for s in open_bundle["sources"]}
|
| 61 |
+
self.assertIn("ev_junk_lowauth_01", open_ids)
|
| 62 |
+
|
| 63 |
+
filtered = retrieve(
|
| 64 |
+
self.query,
|
| 65 |
+
query_embedding=self.qvec,
|
| 66 |
+
index=self.index,
|
| 67 |
+
min_authority=0.5,
|
| 68 |
+
require_parent_evidence=True,
|
| 69 |
+
top_k=10,
|
| 70 |
+
)
|
| 71 |
+
filt_ids = {s["evidence_id"] for s in filtered["sources"]}
|
| 72 |
+
self.assertNotIn("ev_junk_lowauth_01", filt_ids)
|
| 73 |
+
# Every returned source has provenance fields
|
| 74 |
+
for src in filtered["sources"]:
|
| 75 |
+
self.assertIn("content_hash", src)
|
| 76 |
+
self.assertIn("source_authority", src)
|
| 77 |
+
self.assertIn("parent_evidence", src)
|
| 78 |
+
self.assertTrue(src["parent_evidence"])
|
| 79 |
+
|
| 80 |
+
def test_hybrid_lexical_can_lift_weak_dense(self):
|
| 81 |
+
"""Rule: score = dense + lexical + authority - penalties (no dense floor).
|
| 82 |
+
|
| 83 |
+
ev_lexical_lift_01 has weak cosine vs the COBOL query vector but strong
|
| 84 |
+
token overlap on cobol/payment/balance, so it can appear in results.
|
| 85 |
+
"""
|
| 86 |
+
# Dense-only ranking of lexical_lift should be poor
|
| 87 |
+
from evidence.retrieve import cosine_similarity
|
| 88 |
+
|
| 89 |
+
lift = self.index._by_id["ev_lexical_lift_01"]
|
| 90 |
+
dense_lift = cosine_similarity(self.qvec, lift.embedding)
|
| 91 |
+
dense_copy = cosine_similarity(
|
| 92 |
+
self.qvec, self.index._by_id["ev_copybook_01"].embedding
|
| 93 |
+
)
|
| 94 |
+
self.assertLess(dense_lift, dense_copy)
|
| 95 |
+
self.assertLess(dense_lift, 0.55) # weak dense
|
| 96 |
+
|
| 97 |
+
lex = lexical_score(self.query, lift.content)
|
| 98 |
+
self.assertGreaterEqual(lex, 0.5) # strong lexical
|
| 99 |
+
|
| 100 |
+
bundle = retrieve(
|
| 101 |
+
self.query,
|
| 102 |
+
query_embedding=self.qvec,
|
| 103 |
+
index=self.index,
|
| 104 |
+
min_authority=0.5,
|
| 105 |
+
require_parent_evidence=True,
|
| 106 |
+
top_k=10,
|
| 107 |
+
)
|
| 108 |
+
ids = [s["evidence_id"] for s in bundle["sources"]]
|
| 109 |
+
self.assertIn(
|
| 110 |
+
"ev_lexical_lift_01",
|
| 111 |
+
ids,
|
| 112 |
+
"lexical-strong / dense-weak chunk should appear under hybrid score",
|
| 113 |
+
)
|
| 114 |
+
# Confirm retrieval_metadata records both components
|
| 115 |
+
meta = {
|
| 116 |
+
m["evidence_id"]: m for m in bundle["retrieval_metadata"]
|
| 117 |
+
}
|
| 118 |
+
self.assertIn("ev_lexical_lift_01", meta)
|
| 119 |
+
self.assertGreaterEqual(meta["ev_lexical_lift_01"]["lexical"], 0.5)
|
| 120 |
+
self.assertLess(meta["ev_lexical_lift_01"]["dense"], 0.55)
|
| 121 |
+
|
| 122 |
+
def test_empty_query_returns_empty_bundle(self):
|
| 123 |
+
bundle = retrieve("", query_embedding=self.qvec, index=self.index)
|
| 124 |
+
self.assertEqual(bundle, empty_bundle())
|
| 125 |
+
self.assertEqual(bundle["sources"], [])
|
| 126 |
+
self.assertEqual(bundle["facts"], [])
|
| 127 |
+
|
| 128 |
+
def test_no_match_returns_empty_bundle(self):
|
| 129 |
+
# Metadata filter that matches nothing
|
| 130 |
+
bundle = retrieve(
|
| 131 |
+
self.query,
|
| 132 |
+
query_embedding=self.qvec,
|
| 133 |
+
index=self.index,
|
| 134 |
+
source_types=["nonexistent_type"],
|
| 135 |
+
top_k=5,
|
| 136 |
+
)
|
| 137 |
+
self.assertEqual(bundle["sources"], [])
|
| 138 |
+
self.assertEqual(bundle["facts"], [])
|
| 139 |
+
self.assertEqual(bundle["provenance"], [])
|
| 140 |
+
self.assertEqual(bundle["retrieval_metadata"], [])
|
| 141 |
+
|
| 142 |
+
def test_bundle_shape_has_provenance(self):
|
| 143 |
+
bundle = retrieve(
|
| 144 |
+
self.query,
|
| 145 |
+
query_embedding=self.qvec,
|
| 146 |
+
index=self.index,
|
| 147 |
+
min_authority=0.5,
|
| 148 |
+
top_k=3,
|
| 149 |
+
)
|
| 150 |
+
for key in ("facts", "sources", "provenance", "retrieval_metadata"):
|
| 151 |
+
self.assertIn(key, bundle)
|
| 152 |
+
self.assertEqual(len(bundle["sources"]), len(bundle["provenance"]))
|
| 153 |
+
# No bare text: each source carries provenance fields
|
| 154 |
+
for src in bundle["sources"]:
|
| 155 |
+
self.assertTrue(src.get("content_hash"))
|
| 156 |
+
self.assertIsNotNone(src.get("source_authority"))
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
if __name__ == "__main__":
|
| 160 |
+
unittest.main()
|
evidence/write_router_smoke.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Append a router smoke log line using frozen scorer + Spec symbolic.
|
| 2 |
+
|
| 3 |
+
Does not modify router/scorer.py or router/symbolic.py.
|
| 4 |
+
Scores come only from scorer.score_agent (via symbolic.minimum_agent_set).
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import sys
|
| 10 |
+
from datetime import datetime, timezone
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
# David root on path so `evidence` and `router` imports resolve
|
| 14 |
+
DAVID_ROOT = Path(__file__).resolve().parents[1]
|
| 15 |
+
sys.path.insert(0, str(DAVID_ROOT))
|
| 16 |
+
sys.path.insert(0, str(DAVID_ROOT / "router"))
|
| 17 |
+
|
| 18 |
+
from scorer import AgentDescriptor, RequestContext, score_agent # noqa: E402
|
| 19 |
+
from symbolic import minimum_agent_set # noqa: E402
|
| 20 |
+
|
| 21 |
+
from evidence.retrieve import ( # noqa: E402
|
| 22 |
+
COBOL_BALANCE_QUERY,
|
| 23 |
+
COBOL_BALANCE_QUERY_VEC,
|
| 24 |
+
EvidenceIndex,
|
| 25 |
+
retrieve,
|
| 26 |
+
seed_cobol_corpus,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
LOG_PATH = DAVID_ROOT / "logs" / "router_smoke.log"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _agent(aid, caps, deps=(), control=False, risk="medium", perms=(), inputs=()):
|
| 33 |
+
"""Same helper shape as router/test_scorer.py."""
|
| 34 |
+
return AgentDescriptor(
|
| 35 |
+
agent_id=aid,
|
| 36 |
+
domain="test",
|
| 37 |
+
capabilities=frozenset(caps),
|
| 38 |
+
input_types=frozenset(inputs or ("source_code",)),
|
| 39 |
+
permissions=frozenset(perms or ("read_source",)),
|
| 40 |
+
risk_class=risk,
|
| 41 |
+
dependencies=frozenset(deps),
|
| 42 |
+
historical_success=0.5,
|
| 43 |
+
control_plane=control,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def cobol_request_context():
|
| 48 |
+
"""COBOL-style RequestContext from router/test_scorer.py."""
|
| 49 |
+
registry = {
|
| 50 |
+
"LegacyCobol": _agent(
|
| 51 |
+
"LegacyCobol",
|
| 52 |
+
["cobol", "business_rule_extraction"],
|
| 53 |
+
deps=("Database", "Provenance"),
|
| 54 |
+
risk="high",
|
| 55 |
+
),
|
| 56 |
+
"Database": _agent("Database", ["schema_analysis"], inputs=("db_schema",)),
|
| 57 |
+
"Provenance": _agent("Provenance", ["provenance_dag"]),
|
| 58 |
+
"Quant": _agent("Quant", ["pricing", "monte_carlo"]),
|
| 59 |
+
"Risk": _agent("Risk", ["risk_policy"], control=True, risk="critical"),
|
| 60 |
+
"Response": _agent("Response", ["synthesis"], control=True),
|
| 61 |
+
}
|
| 62 |
+
candidates = list(registry.values())
|
| 63 |
+
req = RequestContext(
|
| 64 |
+
required_capabilities=frozenset({"cobol", "business_rule_extraction"}),
|
| 65 |
+
available_inputs=frozenset({"source_code", "copybooks"}),
|
| 66 |
+
granted_permissions=frozenset({"read_source", "read_copybooks"}),
|
| 67 |
+
max_risk_class="high",
|
| 68 |
+
semantic_scores={
|
| 69 |
+
"LegacyCobol": 0.92,
|
| 70 |
+
"Database": 0.4,
|
| 71 |
+
"Quant": 0.1,
|
| 72 |
+
"Provenance": 0.3,
|
| 73 |
+
"Risk": 0.2,
|
| 74 |
+
"Response": 0.2,
|
| 75 |
+
},
|
| 76 |
+
)
|
| 77 |
+
return registry, candidates, req
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def write_smoke_log(path: Path | None = None) -> Path:
|
| 81 |
+
log_path = path or LOG_PATH
|
| 82 |
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
| 83 |
+
|
| 84 |
+
registry, candidates, req = cobol_request_context()
|
| 85 |
+
# Prefer Spec's minimum_agent_set (uses scorer.score_agent only)
|
| 86 |
+
selected = minimum_agent_set(candidates, req, registry)
|
| 87 |
+
# Touch score_agent explicitly so the smoke note is accurate
|
| 88 |
+
_ = {a.agent_id: score_agent(a, req) for a in candidates}
|
| 89 |
+
|
| 90 |
+
bundle = retrieve(
|
| 91 |
+
COBOL_BALANCE_QUERY,
|
| 92 |
+
query_embedding=COBOL_BALANCE_QUERY_VEC,
|
| 93 |
+
index=EvidenceIndex(seed_cobol_corpus()),
|
| 94 |
+
min_authority=0.5,
|
| 95 |
+
require_parent_evidence=True,
|
| 96 |
+
top_k=5,
|
| 97 |
+
)
|
| 98 |
+
top_eids = [s["evidence_id"] for s in bundle["sources"]]
|
| 99 |
+
|
| 100 |
+
# Timestamp in America/Los_Angeles for operator-facing logs
|
| 101 |
+
try:
|
| 102 |
+
from zoneinfo import ZoneInfo
|
| 103 |
+
|
| 104 |
+
ts = datetime.now(ZoneInfo("America/Los_Angeles")).strftime(
|
| 105 |
+
"%Y-%m-%d %H:%M:%S %Z"
|
| 106 |
+
)
|
| 107 |
+
except Exception:
|
| 108 |
+
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
| 109 |
+
|
| 110 |
+
quant_absent = "Quant" not in selected
|
| 111 |
+
risk_present = "Risk" in selected
|
| 112 |
+
response_present = "Response" in selected
|
| 113 |
+
|
| 114 |
+
lines = [
|
| 115 |
+
f"[{ts}] router_smoke minimum_agent_set selected={selected}",
|
| 116 |
+
f"[{ts}] Quant absent={quant_absent}; Risk present={risk_present}; "
|
| 117 |
+
f"Response present={response_present}",
|
| 118 |
+
f"[{ts}] note: scores came from scorer.score_agent only "
|
| 119 |
+
f"(via symbolic.minimum_agent_set; no second scorer)",
|
| 120 |
+
f"[{ts}] evidence_smoke query={COBOL_BALANCE_QUERY!r} "
|
| 121 |
+
f"top_evidence_ids={top_eids}",
|
| 122 |
+
"",
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
+
with log_path.open("a", encoding="utf-8") as fh:
|
| 126 |
+
fh.write("\n".join(lines))
|
| 127 |
+
|
| 128 |
+
return log_path
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
if __name__ == "__main__":
|
| 132 |
+
out = write_smoke_log()
|
| 133 |
+
print(f"appended router smoke log β {out}")
|
execution/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sparse agent execution: WorkItem in, AgentResult out, selected set only."""
|
| 2 |
+
|
| 3 |
+
from .dispatcher import (
|
| 4 |
+
ExecutionRequest,
|
| 5 |
+
build_work_items,
|
| 6 |
+
dispatch,
|
| 7 |
+
stub_handler,
|
| 8 |
+
)
|
| 9 |
+
from .models import AgentResult, Evidence, EvidenceBundle, WorkItem
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
"AgentResult",
|
| 13 |
+
"Evidence",
|
| 14 |
+
"EvidenceBundle",
|
| 15 |
+
"ExecutionRequest",
|
| 16 |
+
"WorkItem",
|
| 17 |
+
"build_work_items",
|
| 18 |
+
"dispatch",
|
| 19 |
+
"stub_handler",
|
| 20 |
+
]
|
execution/dispatcher.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Execute only the sparse-selected agent set β no swarm fan-out."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import uuid
|
| 6 |
+
from collections.abc import Callable, Mapping, Sequence
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
|
| 9 |
+
from .models import AgentResult, EvidenceBundle, WorkItem
|
| 10 |
+
|
| 11 |
+
# Handler: (WorkItem) -> AgentResult
|
| 12 |
+
AgentHandler = Callable[[WorkItem], AgentResult]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass(frozen=True)
|
| 16 |
+
class ExecutionRequest:
|
| 17 |
+
request_id: str
|
| 18 |
+
task: str
|
| 19 |
+
evidence_bundle: EvidenceBundle
|
| 20 |
+
required_outputs: tuple[str, ...]
|
| 21 |
+
permissions: frozenset[str]
|
| 22 |
+
risk_policy: str
|
| 23 |
+
deadline: str | None = None
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def build_work_items(
|
| 27 |
+
selected_agent_ids: Sequence[str],
|
| 28 |
+
req: ExecutionRequest,
|
| 29 |
+
) -> list[WorkItem]:
|
| 30 |
+
"""One WorkItem per selected agent_id. Order preserved. No extras."""
|
| 31 |
+
if not selected_agent_ids:
|
| 32 |
+
return []
|
| 33 |
+
# Deduplicate while preserving order (router may close deps + control plane).
|
| 34 |
+
seen: set[str] = set()
|
| 35 |
+
ordered: list[str] = []
|
| 36 |
+
for aid in selected_agent_ids:
|
| 37 |
+
if aid not in seen:
|
| 38 |
+
seen.add(aid)
|
| 39 |
+
ordered.append(aid)
|
| 40 |
+
return [
|
| 41 |
+
WorkItem(
|
| 42 |
+
request_id=req.request_id,
|
| 43 |
+
work_id=f"{req.request_id}:{aid}:{uuid.uuid4().hex[:8]}",
|
| 44 |
+
agent_id=aid,
|
| 45 |
+
task=req.task,
|
| 46 |
+
evidence_bundle=req.evidence_bundle,
|
| 47 |
+
required_outputs=req.required_outputs,
|
| 48 |
+
permissions=req.permissions,
|
| 49 |
+
deadline=req.deadline,
|
| 50 |
+
risk_policy=req.risk_policy,
|
| 51 |
+
)
|
| 52 |
+
for aid in ordered
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def dispatch(
|
| 57 |
+
selected_agent_ids: Sequence[str],
|
| 58 |
+
req: ExecutionRequest,
|
| 59 |
+
handlers: Mapping[str, AgentHandler],
|
| 60 |
+
*,
|
| 61 |
+
allow_missing: bool = False,
|
| 62 |
+
) -> list[AgentResult]:
|
| 63 |
+
"""
|
| 64 |
+
Run handlers only for selected agents.
|
| 65 |
+
|
| 66 |
+
Agents not in selected_agent_ids are never invoked.
|
| 67 |
+
Missing handlers raise KeyError unless allow_missing (then status=skipped).
|
| 68 |
+
"""
|
| 69 |
+
items = build_work_items(selected_agent_ids, req)
|
| 70 |
+
results: list[AgentResult] = []
|
| 71 |
+
for item in items:
|
| 72 |
+
handler = handlers.get(item.agent_id)
|
| 73 |
+
if handler is None:
|
| 74 |
+
if not allow_missing:
|
| 75 |
+
raise KeyError(f"no handler for selected agent {item.agent_id!r}")
|
| 76 |
+
results.append(
|
| 77 |
+
AgentResult(agent_id=item.agent_id, status="skipped")
|
| 78 |
+
)
|
| 79 |
+
continue
|
| 80 |
+
results.append(handler(item))
|
| 81 |
+
return results
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def stub_handler(agent_id: str) -> AgentHandler:
|
| 85 |
+
"""Deterministic stub for wiring tests (not a real agent)."""
|
| 86 |
+
|
| 87 |
+
def _run(item: WorkItem) -> AgentResult:
|
| 88 |
+
assert item.agent_id == agent_id
|
| 89 |
+
return AgentResult(
|
| 90 |
+
agent_id=agent_id,
|
| 91 |
+
conclusions=({"note": f"stub:{agent_id}", "work_id": item.work_id},),
|
| 92 |
+
evidence_refs=tuple(
|
| 93 |
+
e.evidence_id for e in item.evidence_bundle.sources
|
| 94 |
+
),
|
| 95 |
+
provenance_root=f"prov:{item.request_id}:{agent_id}",
|
| 96 |
+
status="ok",
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
return _run
|
execution/models.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""WorkItem / AgentResult / EvidenceBundle contracts for sparse execution."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import asdict, dataclass, field
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass(frozen=True)
|
| 10 |
+
class Evidence:
|
| 11 |
+
evidence_id: str
|
| 12 |
+
source_id: str
|
| 13 |
+
source_type: str
|
| 14 |
+
document_id: str
|
| 15 |
+
location: str
|
| 16 |
+
content_hash: str
|
| 17 |
+
embedding_id: str | None = None
|
| 18 |
+
retrieved_by: str | None = None
|
| 19 |
+
retrieval_score: float | None = None
|
| 20 |
+
source_authority: str | None = None
|
| 21 |
+
timestamp: str | None = None
|
| 22 |
+
parent_evidence: tuple[str, ...] = ()
|
| 23 |
+
content: str | None = None # never delivered without the fields above
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass(frozen=True)
|
| 27 |
+
class EvidenceBundle:
|
| 28 |
+
facts: tuple[dict[str, Any], ...] = ()
|
| 29 |
+
sources: tuple[Evidence, ...] = ()
|
| 30 |
+
provenance: tuple[dict[str, Any], ...] = ()
|
| 31 |
+
retrieval_metadata: tuple[dict[str, Any], ...] = ()
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass(frozen=True)
|
| 35 |
+
class WorkItem:
|
| 36 |
+
request_id: str
|
| 37 |
+
work_id: str
|
| 38 |
+
agent_id: str
|
| 39 |
+
task: str
|
| 40 |
+
evidence_bundle: EvidenceBundle
|
| 41 |
+
required_outputs: tuple[str, ...]
|
| 42 |
+
permissions: frozenset[str]
|
| 43 |
+
deadline: str | None
|
| 44 |
+
risk_policy: str
|
| 45 |
+
|
| 46 |
+
def to_dict(self) -> dict[str, Any]:
|
| 47 |
+
d = asdict(self)
|
| 48 |
+
d["permissions"] = sorted(self.permissions)
|
| 49 |
+
return d
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@dataclass(frozen=True)
|
| 53 |
+
class AgentResult:
|
| 54 |
+
agent_id: str
|
| 55 |
+
facts: tuple[dict[str, Any], ...] = ()
|
| 56 |
+
conclusions: tuple[dict[str, Any], ...] = ()
|
| 57 |
+
assumptions: tuple[str, ...] = ()
|
| 58 |
+
uncertainty: tuple[str, ...] = ()
|
| 59 |
+
evidence_refs: tuple[str, ...] = ()
|
| 60 |
+
transformations: tuple[dict[str, Any], ...] = ()
|
| 61 |
+
risk_signals: tuple[str, ...] = ()
|
| 62 |
+
provenance_root: str | None = None
|
| 63 |
+
status: str = "ok" # ok | failed | escalated | skipped
|
| 64 |
+
|
| 65 |
+
def to_dict(self) -> dict[str, Any]:
|
| 66 |
+
return asdict(self)
|
execution/test_dispatcher.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Smoke: only selected agents receive WorkItems and run."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import unittest
|
| 6 |
+
|
| 7 |
+
from execution.models import Evidence, EvidenceBundle
|
| 8 |
+
from execution.dispatcher import (
|
| 9 |
+
ExecutionRequest,
|
| 10 |
+
build_work_items,
|
| 11 |
+
dispatch,
|
| 12 |
+
stub_handler,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
SELECTED = [
|
| 17 |
+
"LegacyCobol",
|
| 18 |
+
"Database",
|
| 19 |
+
"ReverseEngineering",
|
| 20 |
+
"Provenance",
|
| 21 |
+
"Risk",
|
| 22 |
+
"Response",
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class TestSparseDispatch(unittest.TestCase):
|
| 27 |
+
def setUp(self) -> None:
|
| 28 |
+
self.bundle = EvidenceBundle(
|
| 29 |
+
sources=(
|
| 30 |
+
Evidence(
|
| 31 |
+
evidence_id="e1",
|
| 32 |
+
source_id="s1",
|
| 33 |
+
source_type="cobol_source",
|
| 34 |
+
document_id="pay.cbl",
|
| 35 |
+
location="lines:100-200",
|
| 36 |
+
content_hash="abc",
|
| 37 |
+
content="COMPUTE BALANCE ...",
|
| 38 |
+
),
|
| 39 |
+
),
|
| 40 |
+
)
|
| 41 |
+
self.req = ExecutionRequest(
|
| 42 |
+
request_id="req-cobol-1",
|
| 43 |
+
task=(
|
| 44 |
+
"Explain how this COBOL payment program calculates "
|
| 45 |
+
"the account balance and determine whether its Java "
|
| 46 |
+
"replacement preserves the behavior."
|
| 47 |
+
),
|
| 48 |
+
evidence_bundle=self.bundle,
|
| 49 |
+
required_outputs=("business_rules", "equivalence_result"),
|
| 50 |
+
permissions=frozenset(
|
| 51 |
+
{"read_source", "read_copybooks", "read_schema", "read_evidence"}
|
| 52 |
+
),
|
| 53 |
+
risk_policy="escalate_on_high",
|
| 54 |
+
)
|
| 55 |
+
self.handlers = {aid: stub_handler(aid) for aid in SELECTED}
|
| 56 |
+
# Quant must never run even if a handler exists.
|
| 57 |
+
self.handlers["Quant"] = stub_handler("Quant")
|
| 58 |
+
|
| 59 |
+
def test_work_items_only_for_selected(self) -> None:
|
| 60 |
+
items = build_work_items(SELECTED, self.req)
|
| 61 |
+
self.assertEqual([i.agent_id for i in items], SELECTED)
|
| 62 |
+
self.assertTrue(all(i.request_id == "req-cobol-1" for i in items))
|
| 63 |
+
self.assertNotIn("Quant", [i.agent_id for i in items])
|
| 64 |
+
|
| 65 |
+
def test_dispatch_invokes_only_selected(self) -> None:
|
| 66 |
+
invoked: list[str] = []
|
| 67 |
+
|
| 68 |
+
def wrap(aid: str):
|
| 69 |
+
inner = stub_handler(aid)
|
| 70 |
+
|
| 71 |
+
def _h(item):
|
| 72 |
+
invoked.append(aid)
|
| 73 |
+
return inner(item)
|
| 74 |
+
|
| 75 |
+
return _h
|
| 76 |
+
|
| 77 |
+
handlers = {aid: wrap(aid) for aid in self.handlers}
|
| 78 |
+
results = dispatch(SELECTED, self.req, handlers)
|
| 79 |
+
self.assertEqual([r.agent_id for r in results], SELECTED)
|
| 80 |
+
self.assertEqual(invoked, SELECTED)
|
| 81 |
+
self.assertNotIn("Quant", invoked)
|
| 82 |
+
self.assertTrue(all(r.status == "ok" for r in results))
|
| 83 |
+
|
| 84 |
+
def test_empty_selection_runs_nobody(self) -> None:
|
| 85 |
+
results = dispatch([], self.req, self.handlers)
|
| 86 |
+
self.assertEqual(results, [])
|
| 87 |
+
|
| 88 |
+
def test_evidence_bundle_not_bare_text(self) -> None:
|
| 89 |
+
items = build_work_items(["LegacyCobol"], self.req)
|
| 90 |
+
src = items[0].evidence_bundle.sources[0]
|
| 91 |
+
self.assertEqual(src.evidence_id, "e1")
|
| 92 |
+
self.assertEqual(src.content_hash, "abc")
|
| 93 |
+
self.assertIsNotNone(src.source_id)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
if __name__ == "__main__":
|
| 97 |
+
unittest.main()
|
j/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# David core (J)
|
| 2 |
+
|
| 3 |
+
Stack lock: **J + PostgreSQL/pgvector + Prolog**.
|
| 4 |
+
|
| 5 |
+
| Path | Role |
|
| 6 |
+
|------|------|
|
| 7 |
+
| `scorer.ijs` | Deterministic score shape (ported from Python harness) |
|
| 8 |
+
| `eligibility.ijs` | **Product** β J β SWI-Prolog eligibility (candidates + request β minimum agent set) |
|
| 9 |
+
| `run_eligibility_smoke.ijs` | Product smoke for `eligibility.ijs` (ANN fixture + Quant negative) |
|
| 10 |
+
| `symbolic_call.ijs` | Thin delegate β `run_eligibility_smoke.ijs` |
|
| 11 |
+
| `execution.ijs` | WorkItem β AgentResult; Prolog selected-set; EvidenceBundle-only |
|
| 12 |
+
| `run_execution_smoke.ijs` | Smoke for `execution.ijs` |
|
| 13 |
+
| `evidence.ijs` | Hybrid evidence retrieve (product; seed + `ev_retrieve` / `ev_smoke`) |
|
| 14 |
+
| `run_evidence_smoke.ijs` | Smoke for `evidence.ijs` |
|
| 15 |
+
| `fixtures/ann_topk.txt` | Fixture candidate ids (ANN top-K shape + Quant negative) |
|
| 16 |
+
| `../sql/001_agent_and_evidence_indexes.sql` | AGENT / EVIDENCE indexes |
|
| 17 |
+
| `../sql/002_evidence_hybrid_retrieve.sql` | Product hybrid retrieve against `evidence` |
|
| 18 |
+
| `../router/symbolic_rules.pl` | Product Prolog eligibility rules |
|
| 19 |
+
| `../router/registry_facts.pl` | Generated agent facts from `registry/seed_agents.yaml` |
|
| 20 |
+
|
| 21 |
+
Python under `../router`, `../execution`, `../evidence` is smoke only β not the product.
|
| 22 |
+
|
| 23 |
+
Foundry J lives separately at `/workspace/foundry-j`.
|
| 24 |
+
|
| 25 |
+
Handoff: `../scripts/prolog_selected_handoff.sh` writes `../logs/selected_cobol.txt` for `load_selected`.
|
| 26 |
+
Product Spec path: `eligibility.ijs` β `swipl` `run_eligibility/1` (not Python filters).
|
j/eligibility.ijs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
NB. SPDX-License-Identifier: AGPL-3.0-only
|
| 2 |
+
NB. Product path: J passes candidates + request into SWI-Prolog eligibility.
|
| 3 |
+
NB. Embeddings propose; Prolog decides. No Python in this path.
|
| 4 |
+
NB. Atom map: registry id β Prolog atom = downcase first letter (LegacyCobolβlegacyCobol).
|
| 5 |
+
|
| 6 |
+
cocurrent 'david'
|
| 7 |
+
|
| 8 |
+
NB. Repo root relative to j/ working directory
|
| 9 |
+
DAVID_ROOT=: '..'
|
| 10 |
+
|
| 11 |
+
NB. Registry PascalCase id β Prolog atom (first char only)
|
| 12 |
+
to_prolog_atom=: 3 : 0
|
| 13 |
+
s=. ,": y
|
| 14 |
+
if. 0=#s do. '' return. end.
|
| 15 |
+
u=. a. i. {. s
|
| 16 |
+
if. (u >: 65) *. (u <: 90) do.
|
| 17 |
+
((u + 32) { a.) , }. s
|
| 18 |
+
else.
|
| 19 |
+
s
|
| 20 |
+
end.
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
NB. Write temporary Prolog request facts (plain clauses; preds are dynamic in rules).
|
| 24 |
+
NB. y = (<req),(<caps),(<perms),(<maxr),(<cands)
|
| 25 |
+
write_request_pl=: 3 : 0
|
| 26 |
+
'req caps perms maxr cands'=. y
|
| 27 |
+
path=. jpath '~temp/david_elig_req.pl'
|
| 28 |
+
try. 1!:55 <path catch. end.
|
| 29 |
+
lines=. 0$a:
|
| 30 |
+
lines=. lines , <('% generated by j/eligibility.ijs β plain facts for dynamic preds')
|
| 31 |
+
for_c. caps do.
|
| 32 |
+
lines=. lines , <('required_capability(', req, ', ', (>c), ').')
|
| 33 |
+
end.
|
| 34 |
+
for_p. perms do.
|
| 35 |
+
lines=. lines , <('granted_permission(', req, ', ', (>p), ').')
|
| 36 |
+
end.
|
| 37 |
+
lines=. lines , <('max_risk_class(', req, ', ', maxr, ').')
|
| 38 |
+
for_a. cands do.
|
| 39 |
+
atom=. to_prolog_atom >a
|
| 40 |
+
lines=. lines , <('ann_candidate(', req, ', ', atom, ').')
|
| 41 |
+
end.
|
| 42 |
+
(; LF ,~ each lines) 1!:2 <path
|
| 43 |
+
path
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
NB. Invoke swipl run_eligibility(Req); return stdout text
|
| 47 |
+
NB. y = (<req),(<path)
|
| 48 |
+
swipl_eligibility=: 3 : 0
|
| 49 |
+
'req reqpl'=. y
|
| 50 |
+
rules=. DAVID_ROOT , '/router/symbolic_rules.pl'
|
| 51 |
+
goal=. 'consult(''', reqpl, '''), run_eligibility(', req, ')'
|
| 52 |
+
cmd=. 'swipl -q -s ' , rules , ' -g "' , goal , '" -t halt'
|
| 53 |
+
2!:0 cmd
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
NB. Parse selected:[a,b,c] line β boxed atom strings
|
| 57 |
+
parse_selected=: 3 : 0
|
| 58 |
+
t=. ,": y
|
| 59 |
+
i=. I. 'selected:' E. t
|
| 60 |
+
if. 0=#i do. 0$<'' return. end.
|
| 61 |
+
rest=. (9 + {.i) }. t
|
| 62 |
+
NB. drop brackets/whitespace; keep commas and atoms
|
| 63 |
+
rest=. rest -. LF , CR , ' []'
|
| 64 |
+
if. 0=#rest do. 0$<'' return. end.
|
| 65 |
+
<;._1 (',', rest)
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
NB. Product entry
|
| 69 |
+
NB. y = (<caps),(<perms),(<max_risk),(<cands_registry_ids)
|
| 70 |
+
NB. returns boxed selected Prolog atoms
|
| 71 |
+
elig_query=: 3 : 0
|
| 72 |
+
'caps perms maxr cands'=. y
|
| 73 |
+
req=. 'j_req'
|
| 74 |
+
path=. write_request_pl (<req),(<caps),(<perms),(<maxr),(<cands)
|
| 75 |
+
out=. swipl_eligibility (<req),(<path)
|
| 76 |
+
try. 1!:55 <path catch. end.
|
| 77 |
+
parse_selected out
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
NB. Load candidate fixture file (one registry id per line; # comments ok)
|
| 81 |
+
load_cand_fixture=: 3 : 0
|
| 82 |
+
p=. y
|
| 83 |
+
if. 0=#p do. p=. 'fixtures/ann_topk.txt' end.
|
| 84 |
+
raw=. 1!:1 <p
|
| 85 |
+
lines=. <;._2 raw , LF
|
| 86 |
+
keep=. 0$a:
|
| 87 |
+
for_L. lines do.
|
| 88 |
+
s=. deb >L
|
| 89 |
+
if. (0=#s) +. '#' = {. s , ' ' do. continue. end.
|
| 90 |
+
keep=. keep , <s
|
| 91 |
+
end.
|
| 92 |
+
keep
|
| 93 |
+
)
|
j/evidence.ijs
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
NB. =============================================================================
|
| 2 |
+
NB. Synthetic David β hybrid evidence retrieve (PURE J)
|
| 3 |
+
NB. Product path against sql/001 evidence indexes; Python evidence/ stays harness.
|
| 4 |
+
NB. Locale: david. Verbs prefixed ev_ to avoid clobbering scorer/execution names.
|
| 5 |
+
NB. Smoke embeddings dim 8; production SQL uses vector(1536).
|
| 6 |
+
NB. =============================================================================
|
| 7 |
+
|
| 8 |
+
cocurrent 'david'
|
| 9 |
+
|
| 10 |
+
NB. --- field accessors (row is boxed list) ------------------------------------
|
| 11 |
+
ev_field =: 4 : '> (x { y)'
|
| 12 |
+
|
| 13 |
+
NB. Open one level if boxed
|
| 14 |
+
ev_unbox1 =: 3 : 'if. 32 = 3!:0 y do. > y else. y end.'
|
| 15 |
+
|
| 16 |
+
NB. --- lowercase + alphanumeric tokens len>1 (lexical stand-in for tsv) -------
|
| 17 |
+
EV_AZ =: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
| 18 |
+
EV_az =: 'abcdefghijklmnopqrstuvwxyz'
|
| 19 |
+
EV_alnum =: EV_az , '0123456789'
|
| 20 |
+
|
| 21 |
+
ev_lc1 =: 3 : 0
|
| 22 |
+
i =. EV_AZ i. y
|
| 23 |
+
if. i < #EV_AZ do. i { EV_az else. y end.
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
ev_lc =: 3 : 'ev_lc1"0 y'
|
| 27 |
+
|
| 28 |
+
ev_tokens =: 3 : 0
|
| 29 |
+
if. 0 = #y do. 0 $ a: return. end.
|
| 30 |
+
s =. ev_lc , y
|
| 31 |
+
ok =. s e. EV_alnum
|
| 32 |
+
s =. ok } (' ' #~ #s) ,: s
|
| 33 |
+
parts =. <;._1 ' ' , s
|
| 34 |
+
parts =. parts #~ 1 < #&> parts
|
| 35 |
+
~. parts
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
NB. lexical overlap = |q β© c| / |q| (0 if empty query tokens)
|
| 39 |
+
ev_lexical =: 4 : 0
|
| 40 |
+
q =. ev_tokens x
|
| 41 |
+
if. 0 = #q do. 0.0 return. end.
|
| 42 |
+
c =. ev_tokens y
|
| 43 |
+
if. 0 = #c do. 0.0 return. end.
|
| 44 |
+
(# q -. q -. c) % #q
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
NB. cosine similarity of two numeric lists
|
| 48 |
+
ev_cosine =: 4 : 0
|
| 49 |
+
a =. , x
|
| 50 |
+
b =. , y
|
| 51 |
+
if. (#a) ~: #b do. 0.0 return. end.
|
| 52 |
+
if. (0 = #a) +. (0 = #b) do. 0.0 return. end.
|
| 53 |
+
na =. %: +/ *: a
|
| 54 |
+
nb =. %: +/ *: b
|
| 55 |
+
if. (0 = na) +. (0 = nb) do. 0.0 return. end.
|
| 56 |
+
(+/ a * b) % (na * nb)
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
NB. Axes for dim-8 smoke query embed (prod = vector(1536) via encoder)
|
| 60 |
+
NB. [cobol, balance, payment, schema, pricing, vol, greek, junk]
|
| 61 |
+
EV_AXES =: 'cobol';'balance';'payment';'schema';'pricing';'vol';'greek';'junk'
|
| 62 |
+
|
| 63 |
+
ev_qembed =: 3 : 0
|
| 64 |
+
toks =. ev_tokens y
|
| 65 |
+
NB. synonym: volatility β vol
|
| 66 |
+
if. (<'volatility') e. toks do. toks =. toks , <'vol' end.
|
| 67 |
+
v =. 0 #~ #EV_AXES
|
| 68 |
+
i =. 0
|
| 69 |
+
while. i < #EV_AXES do.
|
| 70 |
+
if. (i { EV_AXES) e. toks do. v =. 1 i} v end.
|
| 71 |
+
i =. i + 1
|
| 72 |
+
end.
|
| 73 |
+
NB. fallback canned COBOL qvec when axis hits empty but query non-blank
|
| 74 |
+
if. (0 = +/ v) *. (0 < #y) do.
|
| 75 |
+
v =. 0.93 0.91 0.88 0.25 0.05 0 0 0
|
| 76 |
+
end.
|
| 77 |
+
v
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
NB. --- seed corpus (mirrors sql/003 + Python harness columns) -----------------
|
| 81 |
+
NB. row: id ; source_id ; source_type ; document_id ; location ;
|
| 82 |
+
NB. content ; authority ; parent_boxed ; embedding (dim 8 smoke)
|
| 83 |
+
|
| 84 |
+
ev_mk =: 3 : 0
|
| 85 |
+
NB. y already a 9-field boxed row
|
| 86 |
+
y
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
EV_CORPUS =: 0 $ a:
|
| 90 |
+
|
| 91 |
+
NB. ev_copybook_01
|
| 92 |
+
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)
|
| 93 |
+
|
| 94 |
+
NB. ev_cobol_src_01
|
| 95 |
+
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)
|
| 96 |
+
|
| 97 |
+
NB. ev_db2_schema_01
|
| 98 |
+
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)
|
| 99 |
+
|
| 100 |
+
NB. ev_quant_pricing_01
|
| 101 |
+
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)
|
| 102 |
+
|
| 103 |
+
NB. ev_lexical_lift_01 β weak dense, strong lexical (no dense floor)
|
| 104 |
+
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)
|
| 105 |
+
|
| 106 |
+
NB. ev_junk_low_auth β authority ~0.1, no parents
|
| 107 |
+
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)
|
| 108 |
+
|
| 109 |
+
NB. --- score one row ----------------------------------------------------------
|
| 110 |
+
NB. score = dense + lexical + authority - parent_penalty
|
| 111 |
+
NB. (no dense floor β lexical can lift weak dense)
|
| 112 |
+
NB. x = query ; qvec ; require_parent y = corpus row
|
| 113 |
+
ev_score_row =: 4 : 0
|
| 114 |
+
q =. 0 ev_field x
|
| 115 |
+
qv =. 1 ev_field x
|
| 116 |
+
reqp =. 2 ev_field x
|
| 117 |
+
row =. y
|
| 118 |
+
emb =. 8 ev_field row
|
| 119 |
+
content =. 5 ev_field row
|
| 120 |
+
auth =. 6 ev_field row
|
| 121 |
+
parents =. boxopen 7 ev_field row
|
| 122 |
+
dense =. qv ev_cosine emb
|
| 123 |
+
lex =. q ev_lexical content
|
| 124 |
+
pen =. 0.0
|
| 125 |
+
if. reqp *. (0 = # parents) do. pen =. 1.0 end.
|
| 126 |
+
dense + lex + auth - pen
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
NB. --- retrieve ---------------------------------------------------------------
|
| 130 |
+
NB. y = query ; min_authority ; top_k [; require_parent]
|
| 131 |
+
NB. returns boxed list of (evidence_id ; score) sorted score DESC, id ASC
|
| 132 |
+
ev_retrieve =: 3 : 0
|
| 133 |
+
args =. y
|
| 134 |
+
q =. 0 ev_field args
|
| 135 |
+
min_a =. 1 ev_field args
|
| 136 |
+
top_k =. 2 ev_field args
|
| 137 |
+
reqp =. 0
|
| 138 |
+
if. 3 < # args do. reqp =. 3 ev_field args end.
|
| 139 |
+
NB. empty / blank query β empty result
|
| 140 |
+
if. 0 = # q do. 0 $ a: return. end.
|
| 141 |
+
if. 0 = # ev_tokens q do. 0 $ a: return. end.
|
| 142 |
+
qv =. ev_qembed q
|
| 143 |
+
ctx =. q ; qv ; reqp
|
| 144 |
+
ids =. 0 $ a:
|
| 145 |
+
scores =. 0 $ 0.0
|
| 146 |
+
i =. 0
|
| 147 |
+
while. i < # EV_CORPUS do.
|
| 148 |
+
row =. ev_unbox1 i { EV_CORPUS
|
| 149 |
+
auth =. 6 ev_field row
|
| 150 |
+
parents =. boxopen 7 ev_field row
|
| 151 |
+
if. auth >: min_a do.
|
| 152 |
+
if. (-. reqp) +. (0 < # parents) do.
|
| 153 |
+
sc =. ctx ev_score_row row
|
| 154 |
+
ids =. ids , < 0 ev_field row
|
| 155 |
+
scores =. scores , sc
|
| 156 |
+
end.
|
| 157 |
+
end.
|
| 158 |
+
i =. i + 1
|
| 159 |
+
end.
|
| 160 |
+
if. 0 = # ids do. 0 $ a: return. end.
|
| 161 |
+
NB. stable: id asc then score desc β ORDER BY score DESC, evidence_id
|
| 162 |
+
ix =. /: ids
|
| 163 |
+
ix =. ix \: ix { scores
|
| 164 |
+
if. top_k < # ix do. ix =. top_k {. ix end.
|
| 165 |
+
out =. 0 $ a:
|
| 166 |
+
j =. 0
|
| 167 |
+
while. j < # ix do.
|
| 168 |
+
k =. j { ix
|
| 169 |
+
out =. out , < ((k { ids) ; k { scores)
|
| 170 |
+
j =. j + 1
|
| 171 |
+
end.
|
| 172 |
+
out
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
NB. peel nested boxes to open string
|
| 176 |
+
ev_openstr =: 3 : 0
|
| 177 |
+
v =. y
|
| 178 |
+
while. 32 = 3!:0 v do. v =. > v end.
|
| 179 |
+
v
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
NB. ids only from retrieve result (single-boxed strings)
|
| 183 |
+
ev_ids =: 3 : 0
|
| 184 |
+
rs =. y
|
| 185 |
+
out =. 0 $ a:
|
| 186 |
+
i =. 0
|
| 187 |
+
while. i < # rs do.
|
| 188 |
+
out =. out , < ev_openstr 0 ev_field ev_unbox1 i { rs
|
| 189 |
+
i =. i + 1
|
| 190 |
+
end.
|
| 191 |
+
out
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
NB. --- smoke ------------------------------------------------------------------
|
| 195 |
+
ev_smoke =: 3 : 0
|
| 196 |
+
q =. 'cobol account payment balance CALC-BALANCE'
|
| 197 |
+
NB. 1) COBOL-ish ranks copybook/cobol/db2 above quant; quant not #1
|
| 198 |
+
r1 =. ev_retrieve q ; 0.5 ; 5 ; 1
|
| 199 |
+
ids1 =. ev_ids r1
|
| 200 |
+
assert. 0 < # ids1
|
| 201 |
+
assert. -. (< 'ev_quant_pricing_01') -: 0 { ids1
|
| 202 |
+
legacy =. 'ev_copybook_01' ; 'ev_cobol_src_01' ; 'ev_db2_schema_01'
|
| 203 |
+
assert. 0 < # (ids1 -. ids1 -. legacy)
|
| 204 |
+
top3 =. 3 <. # ids1
|
| 205 |
+
top3 =. top3 {. ids1
|
| 206 |
+
assert. -. (< 'ev_quant_pricing_01') e. top3
|
| 207 |
+
NB. 2) min_authority 0.5 drops junk
|
| 208 |
+
r2 =. ev_retrieve q ; 0.5 ; 10 ; 0
|
| 209 |
+
ids2 =. ev_ids r2
|
| 210 |
+
assert. -. (< 'ev_junk_low_auth') e. ids2
|
| 211 |
+
r2open =. ev_retrieve q ; 0.0 ; 10 ; 0
|
| 212 |
+
ids2o =. ev_ids r2open
|
| 213 |
+
assert. (< 'ev_junk_low_auth') e. ids2o
|
| 214 |
+
NB. 3) empty query β empty result
|
| 215 |
+
r3 =. ev_retrieve '' ; 0.0 ; 10
|
| 216 |
+
assert. 0 = # r3
|
| 217 |
+
'ok' ; ids1
|
| 218 |
+
)
|
j/execution.ijs
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
NB. =============================================================================
|
| 3 |
+
NB. Synthetic David β WorkItem / AgentResult sparse execution (PURE J)
|
| 4 |
+
NB. Locale: david. Selected agents only β no swarm fan-out.
|
| 5 |
+
NB. Product path: Prolog selected-set β dispatch_selected; EvidenceBundle only.
|
| 6 |
+
NB. Python ../execution stays smoke harness only.
|
| 7 |
+
NB. =============================================================================
|
| 8 |
+
|
| 9 |
+
cocurrent 'david'
|
| 10 |
+
|
| 11 |
+
NB. Evidence row (boxed fields):
|
| 12 |
+
NB. 0 evidence_id 1 source_id 2 source_type 3 content_hash 4 authority
|
| 13 |
+
NB.
|
| 14 |
+
NB. WorkItem (boxed fields):
|
| 15 |
+
NB. 0 request_id 1 work_id 2 agent_id 3 task
|
| 16 |
+
NB. 4 evidence_rows 5 required_outputs 6 permissions 7 deadline 8 risk_policy
|
| 17 |
+
NB.
|
| 18 |
+
NB. AgentResult (boxed fields):
|
| 19 |
+
NB. 0 agent_id 1 status 2 conclusions 3 evidence_refs 4 provenance_root
|
| 20 |
+
|
| 21 |
+
openscalar =: 3 : 0
|
| 22 |
+
v =. y
|
| 23 |
+
while. (32 = 3!:0 v) *. (0 = # $ v) do. v =. > v end.
|
| 24 |
+
v
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
field =: 4 : 'openscalar x { y'
|
| 28 |
+
|
| 29 |
+
NB. Non-empty character / open content
|
| 30 |
+
nonempty =: 3 : 0
|
| 31 |
+
v =. openscalar y
|
| 32 |
+
if. 32 = 3!:0 v do. 0 return. end.
|
| 33 |
+
0 < # v
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
NB. One evidence row is ok iff evidence_id, source_id, content_hash are non-empty.
|
| 37 |
+
NB. Bare text alone is never enough.
|
| 38 |
+
evidence_row_ok =: 3 : 0
|
| 39 |
+
r =. y
|
| 40 |
+
if. 3 > # r do. 0 return. end.
|
| 41 |
+
(nonempty 0 { r) *. (nonempty 1 { r) *. (nonempty 3 { r)
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
NB. Bundle = list of boxed evidence rows; empty bundle is not ok for dispatch.
|
| 45 |
+
bundle_ok =: 3 : 0
|
| 46 |
+
rows =. y
|
| 47 |
+
if. 0 = # rows do. 0 return. end.
|
| 48 |
+
i =. 0
|
| 49 |
+
ok =. 1
|
| 50 |
+
while. i < # rows do.
|
| 51 |
+
ok =. ok *. evidence_row_ok openscalar i { rows
|
| 52 |
+
i =. i + 1
|
| 53 |
+
end.
|
| 54 |
+
ok
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
uniqseq =: 3 : 0
|
| 58 |
+
if. 0 = # y do. 0 $ a: return. end.
|
| 59 |
+
ys =. boxopen y
|
| 60 |
+
out =. 0 $ a:
|
| 61 |
+
i =. 0
|
| 62 |
+
while. i < # ys do.
|
| 63 |
+
v =. openscalar i { ys
|
| 64 |
+
if. -. (< v) e. out do. out =. out , < v end.
|
| 65 |
+
i =. i + 1
|
| 66 |
+
end.
|
| 67 |
+
out
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
fields =: 4 : 0
|
| 71 |
+
rows =. y
|
| 72 |
+
i =. 0
|
| 73 |
+
out =. 0 $ a:
|
| 74 |
+
while. i < # rows do.
|
| 75 |
+
row =. openscalar i { rows
|
| 76 |
+
out =. out , < x field row
|
| 77 |
+
i =. i + 1
|
| 78 |
+
end.
|
| 79 |
+
out
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
NB. Load selected agent ids from a text file (one id per line).
|
| 83 |
+
NB. Product handoff from Prolog / Spec Jβswipl writer.
|
| 84 |
+
load_selected =: 3 : 0
|
| 85 |
+
if. 0 = # (1!:0 < y) do. 0 $ a: return. end.
|
| 86 |
+
raw =. 1!:1 < y
|
| 87 |
+
lines =. <;._2 raw , LF
|
| 88 |
+
out =. 0 $ a:
|
| 89 |
+
i =. 0
|
| 90 |
+
while. i < # lines do.
|
| 91 |
+
s =. openscalar i { lines
|
| 92 |
+
NB. drop CR if present (Windows handoff)
|
| 93 |
+
if. (2 = 3!:0 s) *. (0 < # s) do.
|
| 94 |
+
if. 13 = {: s do. s =. }: s end.
|
| 95 |
+
end.
|
| 96 |
+
if. (2 = 3!:0 s) *. (0 < # s) do. out =. out , < s end.
|
| 97 |
+
i =. i + 1
|
| 98 |
+
end.
|
| 99 |
+
uniqseq out
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
NB. Evidence ids from validated bundle rows
|
| 103 |
+
bundle_ids =: 3 : 0
|
| 104 |
+
rows =. y
|
| 105 |
+
out =. 0 $ a:
|
| 106 |
+
i =. 0
|
| 107 |
+
while. i < # rows do.
|
| 108 |
+
r =. openscalar i { rows
|
| 109 |
+
out =. out , < 0 field r
|
| 110 |
+
i =. i + 1
|
| 111 |
+
end.
|
| 112 |
+
out
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
NB. x = request: request_id ; task ; evidence_bundle ; required_outputs ; permissions ; risk_policy ; deadline
|
| 116 |
+
NB. y = selected agent_id list (from Prolog)
|
| 117 |
+
NB. Fails assert if bundle is not EvidenceBundle-ok (no bare text).
|
| 118 |
+
build_work_items =: 4 : 0
|
| 119 |
+
bundle =. 2 field x
|
| 120 |
+
NB. field peels the outer box; bundle is a list of evidence rows
|
| 121 |
+
if. (32 = 3!:0 bundle) *. (0 = # $ bundle) do. bundle =. , < openscalar bundle end.
|
| 122 |
+
assert. bundle_ok bundle
|
| 123 |
+
aids =. uniqseq y
|
| 124 |
+
rid =. 0 field x
|
| 125 |
+
task =. 1 field x
|
| 126 |
+
outs =. boxopen 3 field x
|
| 127 |
+
perms =. boxopen 4 field x
|
| 128 |
+
rpol =. 5 field x
|
| 129 |
+
dl =. 6 field x
|
| 130 |
+
wis =. 0 $ a:
|
| 131 |
+
i =. 0
|
| 132 |
+
while. i < # aids do.
|
| 133 |
+
aid =. openscalar i { aids
|
| 134 |
+
wid =. rid , ':' , aid
|
| 135 |
+
wi =. (<rid) ; (<wid) ; (<aid) ; (<task) ; (<bundle) ; (<outs) ; (<perms) ; (<dl) ; (<rpol)
|
| 136 |
+
wis =. wis , < wi
|
| 137 |
+
i =. i + 1
|
| 138 |
+
end.
|
| 139 |
+
wis
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
make_stub =: 3 : 0
|
| 143 |
+
aid =. 2 field y
|
| 144 |
+
rid =. 0 field y
|
| 145 |
+
brows =. boxopen 4 field y
|
| 146 |
+
eids =. bundle_ids brows
|
| 147 |
+
(<aid) ; (<'ok') ; (< 'stub:' , aid) ; (<eids) ; (< 'prov:' , rid , ':' , aid)
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
NB. x = request ; y = selected agent ids β AgentResult list (selected only)
|
| 151 |
+
dispatch_selected =: 4 : 0
|
| 152 |
+
wis =. x build_work_items y
|
| 153 |
+
rs =. 0 $ a:
|
| 154 |
+
i =. 0
|
| 155 |
+
while. i < # wis do.
|
| 156 |
+
rs =. rs , < make_stub openscalar i { wis
|
| 157 |
+
i =. i + 1
|
| 158 |
+
end.
|
| 159 |
+
rs
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
NB. Fixture EvidenceBundle rows (ids/hashes/authority β not bare text alone)
|
| 163 |
+
good_bundle =: 3 : 0
|
| 164 |
+
e1 =. (<'ev_cobol_src_01') ; (<'src_cobol') ; (<'cobol_source') ; (<'hash_cobol') ; (<'0.9')
|
| 165 |
+
e2 =. (<'ev_copybook_01') ; (<'src_copy') ; (<'copybook') ; (<'hash_copy') ; (<'0.85')
|
| 166 |
+
e3 =. (<'ev_db2_schema_01') ; (<'src_db2') ; (<'db_schema') ; (<'hash_db2') ; (<'0.8')
|
| 167 |
+
(<e1) , (<e2) , (<e3)
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
bad_bundle_no_hash =: 3 : 0
|
| 171 |
+
e1 =. (<'ev_bare') ; (<'src_x') ; (<'text') ; (<'') ; (<'0.9')
|
| 172 |
+
, < e1
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
smoke =: 3 : 0
|
| 176 |
+
NB. Selected set from Prolog handoff file (written by run_execution_smoke.ijs)
|
| 177 |
+
selected =. load_selected '../logs/selected_cobol.txt'
|
| 178 |
+
assert. 0 < # selected
|
| 179 |
+
assert. (< 'legacyCobol') e. selected
|
| 180 |
+
assert. -. (< 'quant') e. selected
|
| 181 |
+
assert. -. (< 'Quant') e. selected
|
| 182 |
+
|
| 183 |
+
assert. bundle_ok good_bundle 0
|
| 184 |
+
assert. -. bundle_ok bad_bundle_no_hash 0
|
| 185 |
+
assert. -. bundle_ok (0 $ a:)
|
| 186 |
+
|
| 187 |
+
req =. 'req-cobol-1' ; 'Explain COBOL balance vs Java' ; (< good_bundle 0) ; ('business_rules' ; 'equivalence_result') ; ('read_source' ; 'read_copybooks') ; 'escalate_on_high' ; 'none'
|
| 188 |
+
wis =. req build_work_items selected
|
| 189 |
+
wids =. 2 fields wis
|
| 190 |
+
assert. selected -: wids
|
| 191 |
+
assert. -. (< 'quant') e. wids
|
| 192 |
+
rs =. req dispatch_selected selected
|
| 193 |
+
rids =. 0 fields rs
|
| 194 |
+
assert. selected -: rids
|
| 195 |
+
assert. *./ (<'ok') = 1 fields rs
|
| 196 |
+
assert. 0 = # (req dispatch_selected (0 $ a:))
|
| 197 |
+
|
| 198 |
+
NB. Evidence refs on results are bundle ids, not bare text
|
| 199 |
+
refs0 =. boxopen 3 field openscalar 0 { rs
|
| 200 |
+
assert. (< 'ev_cobol_src_01') e. refs0
|
| 201 |
+
|
| 202 |
+
'ok' ; (# selected) ; (# rs) ; (# refs0)
|
| 203 |
+
)
|
j/fixtures/ann_topk.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Fixture: ANN top-K style candidates matching Grok Bot live AGENT smoke
|
| 2 |
+
# plus Quant as a negative (should not be selected for COBOL eligibility).
|
| 3 |
+
LegacyCobol
|
| 4 |
+
ReverseEngineering
|
| 5 |
+
MigrationValidation
|
| 6 |
+
Modernization
|
| 7 |
+
Response
|
| 8 |
+
Quant
|
j/run_eligibility_smoke.ijs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
NB. SPDX-License-Identifier: AGPL-3.0-only
|
| 2 |
+
NB. J β Prolog eligibility product smoke.
|
| 3 |
+
NB. Fixtures: caps cobol+business_rule_extraction; ANN-style candidates + Quant negative.
|
| 4 |
+
NB. Assert: LegacyCobol, Database, Provenance (deps), Risk, Response; Quant absent.
|
| 5 |
+
NB. MUST call exit so jconsole does not hang.
|
| 6 |
+
|
| 7 |
+
(0!:0) <'eligibility.ijs'
|
| 8 |
+
cocurrent 'david'
|
| 9 |
+
|
| 10 |
+
elig_smoke=: 3 : 0
|
| 11 |
+
caps=. 'cobol';'business_rule_extraction'
|
| 12 |
+
perms=. 'read_source';'read_copybooks';'read_schema';'read_evidence';'write_provenance';'read_all_results';'read_conclusions'
|
| 13 |
+
maxr=. 'high'
|
| 14 |
+
cands=. load_cand_fixture 'fixtures/ann_topk.txt'
|
| 15 |
+
echo 'candidates:'; cands
|
| 16 |
+
sel=. elig_query (<caps),(<perms),(<maxr),(<cands)
|
| 17 |
+
echo 'selected:'; sel
|
| 18 |
+
need=. 'legacyCobol';'database';'provenance';'risk';'response'
|
| 19 |
+
missing=. need -. sel
|
| 20 |
+
extra_quant=. (<'quant') e. sel
|
| 21 |
+
if. (0 = #missing) *. -. extra_quant do.
|
| 22 |
+
echo 'eligibility_smoke: OK'
|
| 23 |
+
0
|
| 24 |
+
else.
|
| 25 |
+
if. #missing do. echo 'FAIL missing:'; missing end.
|
| 26 |
+
if. extra_quant do. echo 'FAIL Quant present' end.
|
| 27 |
+
echo 'eligibility_smoke: FAIL'
|
| 28 |
+
1
|
| 29 |
+
end.
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
rc=. elig_smoke 0
|
| 33 |
+
exit rc
|
j/run_evidence_smoke.ijs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(0!:0) <'evidence.ijs'
|
| 2 |
+
cocurrent 'david'
|
| 3 |
+
r =. ev_smoke 0
|
| 4 |
+
echo r
|
| 5 |
+
exit 0
|
j/run_execution_smoke.ijs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
NB. Product smoke: Prolog selected-set β J dispatch_selected + EvidenceBundle-only.
|
| 3 |
+
(13!:0) 0
|
| 4 |
+
2!:0 '../scripts/prolog_selected_handoff.sh'
|
| 5 |
+
(0!:0) <'execution.ijs'
|
| 6 |
+
cocurrent 'david'
|
| 7 |
+
r =. smoke 0
|
| 8 |
+
echo r
|
| 9 |
+
exit 0
|
j/run_smoke.ijs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(0!:0) <'scorer.ijs'
|
| 2 |
+
cocurrent 'david'
|
| 3 |
+
r =. smoke 0
|
| 4 |
+
echo r
|
| 5 |
+
exit 0
|
j/scorer.ijs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
NB. =============================================================================
|
| 2 |
+
NB. Synthetic David β deterministic agent scorer (PURE J)
|
| 3 |
+
NB. Port of router/scorer.py scoring shape. Locale: david
|
| 4 |
+
NB. =============================================================================
|
| 5 |
+
|
| 6 |
+
cocurrent 'david'
|
| 7 |
+
|
| 8 |
+
WPOS =: 1.0 1.2 1.0 0.8 0.6
|
| 9 |
+
WNEG =: 0.9 1.5 0.7
|
| 10 |
+
|
| 11 |
+
RISKLABEL =: 'low';'medium';'high';'critical'
|
| 12 |
+
RISKPEN =: 0.0 0.15 0.35 0.55
|
| 13 |
+
|
| 14 |
+
riskix =: 3 : 0
|
| 15 |
+
i =. RISKLABEL i. <y
|
| 16 |
+
if. i = #RISKLABEL do. 1 else. i end.
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
jaccard =: 4 : 0
|
| 20 |
+
a =. ~. boxopen x
|
| 21 |
+
b =. ~. boxopen y
|
| 22 |
+
if. (0 = #a) *. (0 = #b) do. 1.0 return. end.
|
| 23 |
+
if. (0 = #a) +. (0 = #b) do. 0.0 return. end.
|
| 24 |
+
(# a -. a -. b) % (# ~. a , b)
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
NB. Fetch and open field i from boxed row y
|
| 28 |
+
field =: 4 : '> (x { y)'
|
| 29 |
+
|
| 30 |
+
NB. x = req: caps ; inputs ; perms ; <max_risk
|
| 31 |
+
NB. y = agent row (9 boxed fields)
|
| 32 |
+
score_agent =: 4 : 0
|
| 33 |
+
rcaps =. 0 field x
|
| 34 |
+
ain =. 1 field x
|
| 35 |
+
gperm =. 2 field x
|
| 36 |
+
maxr =. 3 field x
|
| 37 |
+
caps =. 1 field y
|
| 38 |
+
ins =. 2 field y
|
| 39 |
+
perms =. 3 field y
|
| 40 |
+
rcls =. 4 field y
|
| 41 |
+
deps =. 5 field y
|
| 42 |
+
hist =. 0 >. 1 <. (6 field y)
|
| 43 |
+
cp =. 7 field y
|
| 44 |
+
sem =. 8 field y
|
| 45 |
+
cap =. caps jaccard rcaps
|
| 46 |
+
inp =. ins jaccard ain
|
| 47 |
+
dep =. (0 = # boxopen deps) { 0.5 1.0
|
| 48 |
+
miss =. 0 < # (boxopen perms) -. boxopen gperm
|
| 49 |
+
rix =. riskix rcls
|
| 50 |
+
rpen =. rix { RISKPEN
|
| 51 |
+
if. rix > riskix maxr do. rpen =. rpen + 1 end.
|
| 52 |
+
unnec =. -. ((cap > 0) +. (cp ~: 0))
|
| 53 |
+
(+/ WPOS * sem,cap,inp,hist,dep) - +/ WNEG * rpen,miss,unnec
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
smoke =: 3 : 0
|
| 57 |
+
NB. ; boxes each item once β do not pre-box scalars
|
| 58 |
+
req =. ('cobol';'business_rule_extraction') ; ('source_code';'copybooks') ; ('read_source';'read_copybooks') ; 'high'
|
| 59 |
+
legacy =. 'LegacyCobol' ; ('cobol';'business_rule_extraction') ; (<'source_code') ; (<'read_source') ; 'high' ; ('Database';'Provenance') ; 0.5 ; 0 ; 0.92
|
| 60 |
+
quant =. 'Quant' ; ('pricing';'monte_carlo') ; (<'source_code') ; (<'read_source') ; 'medium' ; (0$a:) ; 0.5 ; 0 ; 0.1
|
| 61 |
+
sl =. req score_agent legacy
|
| 62 |
+
sq =. req score_agent quant
|
| 63 |
+
assert. sl > sq
|
| 64 |
+
'ok' ; sl ; sq
|
| 65 |
+
)
|
j/symbolic_call.ijs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
NB. Thin J caller for Spec Prolog eligibility.
|
| 2 |
+
NB. Prefer product path: eligibility.ijs + run_eligibility_smoke.ijs
|
| 3 |
+
NB. This file now delegates to the product smoke (requires swipl on PATH).
|
| 4 |
+
NB. Spawn: /home/box/j/j9.7/bin/jconsole symbolic_call.ijs
|
| 5 |
+
|
| 6 |
+
echo 'symbolic_call.ijs β run_eligibility_smoke.ijs (J+Prolog product path)'
|
| 7 |
+
(0!:0) <'run_eligibility_smoke.ijs'
|
logs/agent_ann_smoke.log
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
query=Explain how this COBOL payment program calculates the account balance and determ...
|
| 2 |
+
top_k=5
|
| 3 |
+
1 LegacyCobol legacy_mainframe high sim=0.406562
|
| 4 |
+
2 ReverseEngineering legacy_mainframe medium sim=0.309016
|
| 5 |
+
3 MigrationValidation migration high sim=0.160817
|
| 6 |
+
4 Modernization migration high sim=0.154508
|
| 7 |
+
5 Response control_plane medium sim=0.000000
|
| 8 |
+
legacy_in_topk=True
|
| 9 |
+
quant_not_preferred=True
|
| 10 |
+
pass=True
|
logs/evidence_hybrid_smoke.log
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
query=Explain how this COBOL payment program calculates the account balance and determ...
|
| 2 |
+
lexical_query=payment balance account
|
| 3 |
+
top_k=5 min_authority=0.5 require_parent=False
|
| 4 |
+
rank evidence_id source_type score dense lexical authority
|
| 5 |
+
1 ev_cobol_src_01 cobol_source score=1.589825 dense=0.583714 lexical=0.106111 authority=0.900000
|
| 6 |
+
2 ev_copybook_01 copybook score=1.323832 dense=0.359546 lexical=0.014286 authority=0.950000
|
| 7 |
+
3 ev_lexical_lift_01 glossary score=1.207805 dense=0.482805 lexical=0.025000 authority=0.700000
|
| 8 |
+
4 ev_db2_schema_01 db2_schema score=1.104701 dense=0.243590 lexical=0.011111 authority=0.850000
|
| 9 |
+
5 ev_quant_pricing_01 quant_report score=1.030079 dense=0.230079 lexical=0.000000 authority=0.800000
|
| 10 |
+
junk_exists=True
|
| 11 |
+
junk_dropped=True
|
| 12 |
+
legacy_in_topk=True
|
| 13 |
+
quant_not_preferred=True
|
| 14 |
+
pass=True
|
logs/j_evidence_smoke.log
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ββββ¬βββββββββββββββ¬ββββββββββββββββ¬βββββββββββββββββ¬βββββββββββββββββββ¬ββββββββββββββββββββ
|
| 2 |
+
βokβev_copybook_01βev_cobol_src_01βev_db2_schema_01βev_lexical_lift_01βev_quant_pricing_01β
|
| 3 |
+
ββββ΄βββββββββββββββ΄ββββββββββββββββ΄βββββββββββββββββ΄βββββββββββββββββββ΄ββββββββββββββββββββ
|
| 4 |
+
exit_code=0
|
| 5 |
+
SQL product query: sql/002_evidence_hybrid_retrieve.sql
|
| 6 |
+
Python evidence/ is harness only β do not grow; J evidence.ijs is the product path.
|
logs/j_execution_smoke.log
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ββββ¬ββ¬ββ¬ββ
|
| 2 |
+
βokβ6β6β3β
|
| 3 |
+
ββββ΄ββ΄ββ΄ββ
|
logs/prolog_eligibility_smoke.log
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
βββββββββββββ¬ββββββββββββ¬βββββββββββββββββββ¬ββββββββββββββββββββ¬ββββββββββββββ¬βββββββββ¬ββββββ
|
| 2 |
+
βcandidates:βLegacyCobolβReverseEngineeringβMigrationValidationβModernizationβResponseβQuantβ
|
| 3 |
+
βββββββββββββ΄ββββββββββββ΄βββββββββββββββββββ΄ββββββββββββββββββββ΄ββββββββββββββ΄βββββββββ΄ββββββ
|
| 4 |
+
βββββββββββ¬ββββββββββββ¬βββββββββ¬βββββββββββββββββββ¬βββββββββββ¬βββββ¬βββββββββ
|
| 5 |
+
βselected:βlegacyCobolβdatabaseβreverseEngineeringβprovenanceβriskβresponseβ
|
| 6 |
+
βββββββββββ΄ββββββββββββ΄βββββββββ΄βββββββββββββββββββ΄βββββββββββ΄βββββ΄βββββββββ
|
| 7 |
+
eligibility_smoke: OK
|
logs/prolog_registry_sync.log
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
wrote /tmp/registry_facts.rqcYnl.pl (10 agents)
|
| 2 |
+
sync ok: 10 agents; Quant present as negative
|
| 3 |
+
prolog_registry_sync: OK
|
logs/prolog_selected_cobol.raw
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
selected:[legacyCobol,database,reverseEngineering,provenance,risk,response]
|
| 2 |
+
smoke_cobol: OK
|
logs/prolog_smoke_cobol.log
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
selected:[legacyCobol,database,reverseEngineering,provenance,risk,response]
|
| 2 |
+
smoke_cobol: OK
|
logs/router_smoke.log
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[2026-10-07 06:14:51 PDT] router_smoke minimum_agent_set selected=['LegacyCobol', 'Database', 'Provenance', 'Risk', 'Response']
|
| 2 |
+
[2026-10-07 06:14:51 PDT] Quant absent=True; Risk present=True; Response present=True
|
| 3 |
+
[2026-10-07 06:14:51 PDT] note: scores came from scorer.score_agent only (via symbolic.minimum_agent_set; no second scorer)
|
| 4 |
+
[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']
|
logs/selected_cobol.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
legacyCobol
|
| 2 |
+
database
|
| 3 |
+
reverseEngineering
|
| 4 |
+
provenance
|
| 5 |
+
risk
|
| 6 |
+
response
|
registry/agent_descriptor.schema.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
| 3 |
+
"$id": "https://david.local/schemas/agent_descriptor.json",
|
| 4 |
+
"title": "AgentDescriptor",
|
| 5 |
+
"type": "object",
|
| 6 |
+
"required": [
|
| 7 |
+
"agent_id",
|
| 8 |
+
"domain",
|
| 9 |
+
"capabilities",
|
| 10 |
+
"languages",
|
| 11 |
+
"input_types",
|
| 12 |
+
"output_types",
|
| 13 |
+
"required_evidence",
|
| 14 |
+
"permissions",
|
| 15 |
+
"risk_class",
|
| 16 |
+
"dependencies",
|
| 17 |
+
"execution_endpoint"
|
| 18 |
+
],
|
| 19 |
+
"properties": {
|
| 20 |
+
"agent_id": { "type": "string", "minLength": 1 },
|
| 21 |
+
"domain": { "type": "string" },
|
| 22 |
+
"capabilities": {
|
| 23 |
+
"type": "array",
|
| 24 |
+
"items": { "type": "string" },
|
| 25 |
+
"uniqueItems": true
|
| 26 |
+
},
|
| 27 |
+
"languages": {
|
| 28 |
+
"type": "array",
|
| 29 |
+
"items": { "type": "string" }
|
| 30 |
+
},
|
| 31 |
+
"input_types": {
|
| 32 |
+
"type": "array",
|
| 33 |
+
"items": { "type": "string" }
|
| 34 |
+
},
|
| 35 |
+
"output_types": {
|
| 36 |
+
"type": "array",
|
| 37 |
+
"items": { "type": "string" }
|
| 38 |
+
},
|
| 39 |
+
"required_evidence": {
|
| 40 |
+
"type": "array",
|
| 41 |
+
"items": { "type": "string" }
|
| 42 |
+
},
|
| 43 |
+
"permissions": {
|
| 44 |
+
"type": "array",
|
| 45 |
+
"items": { "type": "string" }
|
| 46 |
+
},
|
| 47 |
+
"risk_class": {
|
| 48 |
+
"type": "string",
|
| 49 |
+
"enum": ["low", "medium", "high", "critical"]
|
| 50 |
+
},
|
| 51 |
+
"dependencies": {
|
| 52 |
+
"type": "array",
|
| 53 |
+
"items": { "type": "string" },
|
| 54 |
+
"description": "Other agent_id values required for dependency closure"
|
| 55 |
+
},
|
| 56 |
+
"embedding": {
|
| 57 |
+
"type": "array",
|
| 58 |
+
"items": { "type": "number" },
|
| 59 |
+
"description": "Optional; usually stored only in pgvector"
|
| 60 |
+
},
|
| 61 |
+
"execution_endpoint": { "type": "string" },
|
| 62 |
+
"control_plane": {
|
| 63 |
+
"type": "boolean",
|
| 64 |
+
"default": false,
|
| 65 |
+
"description": "If true, always considered for Risk/Audit/Response closure"
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
"additionalProperties": false
|
| 69 |
+
}
|
registry/seed_agents.yaml
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Seed routable agents (not permanently active processes)
|
| 2 |
+
|
| 3 |
+
- agent_id: LegacyCobol
|
| 4 |
+
domain: legacy_mainframe
|
| 5 |
+
capabilities:
|
| 6 |
+
- cobol
|
| 7 |
+
- copybook_analysis
|
| 8 |
+
- control_flow
|
| 9 |
+
- data_flow
|
| 10 |
+
- business_rule_extraction
|
| 11 |
+
- modernization_analysis
|
| 12 |
+
languages: [cobol]
|
| 13 |
+
input_types: [source_code, copybooks, compiler_metadata]
|
| 14 |
+
output_types: [call_graph, business_rules, data_flow]
|
| 15 |
+
required_evidence: [source_code, copybooks, compiler_metadata]
|
| 16 |
+
permissions: [read_source, read_copybooks]
|
| 17 |
+
risk_class: high
|
| 18 |
+
dependencies: [Database, ReverseEngineering, Provenance]
|
| 19 |
+
execution_endpoint: agent://LegacyCobol
|
| 20 |
+
control_plane: false
|
| 21 |
+
|
| 22 |
+
- agent_id: Database
|
| 23 |
+
domain: data
|
| 24 |
+
capabilities: [schema_analysis, db2, sql]
|
| 25 |
+
languages: [sql]
|
| 26 |
+
input_types: [db_schema, queries]
|
| 27 |
+
output_types: [schema_map, data_lineage]
|
| 28 |
+
required_evidence: [db_schema]
|
| 29 |
+
permissions: [read_schema]
|
| 30 |
+
risk_class: medium
|
| 31 |
+
dependencies: []
|
| 32 |
+
execution_endpoint: agent://Database
|
| 33 |
+
|
| 34 |
+
- agent_id: ReverseEngineering
|
| 35 |
+
domain: legacy_mainframe
|
| 36 |
+
capabilities: [call_graph, control_flow, data_flow]
|
| 37 |
+
languages: [cobol, java]
|
| 38 |
+
input_types: [source_code]
|
| 39 |
+
output_types: [call_graph, cfg]
|
| 40 |
+
required_evidence: [source_code]
|
| 41 |
+
permissions: [read_source]
|
| 42 |
+
risk_class: medium
|
| 43 |
+
dependencies: []
|
| 44 |
+
execution_endpoint: agent://ReverseEngineering
|
| 45 |
+
|
| 46 |
+
- agent_id: Modernization
|
| 47 |
+
domain: migration
|
| 48 |
+
capabilities: [cobol_to_java, api_mapping]
|
| 49 |
+
languages: [cobol, java]
|
| 50 |
+
input_types: [business_rules, call_graph]
|
| 51 |
+
output_types: [migration_plan, java_skeleton]
|
| 52 |
+
required_evidence: [business_rules]
|
| 53 |
+
permissions: [read_source, write_plan]
|
| 54 |
+
risk_class: high
|
| 55 |
+
dependencies: [LegacyCobol, Provenance]
|
| 56 |
+
execution_endpoint: agent://Modernization
|
| 57 |
+
|
| 58 |
+
- agent_id: MigrationValidation
|
| 59 |
+
domain: migration
|
| 60 |
+
capabilities: [equivalence, behavioral_compare]
|
| 61 |
+
languages: [cobol, java]
|
| 62 |
+
input_types: [legacy_output, java_output]
|
| 63 |
+
output_types: [equivalence_result]
|
| 64 |
+
required_evidence: [legacy_output, java_output]
|
| 65 |
+
permissions: [read_outputs]
|
| 66 |
+
risk_class: high
|
| 67 |
+
dependencies: [Provenance, LegacyCobol]
|
| 68 |
+
execution_endpoint: agent://MigrationValidation
|
| 69 |
+
|
| 70 |
+
- agent_id: Provenance
|
| 71 |
+
domain: evidence
|
| 72 |
+
capabilities: [provenance_dag, content_hash, authority]
|
| 73 |
+
languages: []
|
| 74 |
+
input_types: [evidence, facts]
|
| 75 |
+
output_types: [provenance_root, evidence_graph]
|
| 76 |
+
required_evidence: []
|
| 77 |
+
permissions: [read_evidence, write_provenance]
|
| 78 |
+
risk_class: medium
|
| 79 |
+
dependencies: []
|
| 80 |
+
execution_endpoint: agent://Provenance
|
| 81 |
+
|
| 82 |
+
- agent_id: Risk
|
| 83 |
+
domain: control_plane
|
| 84 |
+
capabilities: [risk_policy, escalation]
|
| 85 |
+
languages: []
|
| 86 |
+
input_types: [agent_results, risk_signals]
|
| 87 |
+
output_types: [risk_decision]
|
| 88 |
+
required_evidence: []
|
| 89 |
+
permissions: [read_all_results]
|
| 90 |
+
risk_class: critical
|
| 91 |
+
dependencies: []
|
| 92 |
+
execution_endpoint: agent://Risk
|
| 93 |
+
control_plane: true
|
| 94 |
+
|
| 95 |
+
- agent_id: Audit
|
| 96 |
+
domain: control_plane
|
| 97 |
+
capabilities: [audit_trail, compliance]
|
| 98 |
+
languages: []
|
| 99 |
+
input_types: [provenance_root, agent_results]
|
| 100 |
+
output_types: [audit_record]
|
| 101 |
+
required_evidence: []
|
| 102 |
+
permissions: [read_all_results]
|
| 103 |
+
risk_class: high
|
| 104 |
+
dependencies: [Provenance]
|
| 105 |
+
execution_endpoint: agent://Audit
|
| 106 |
+
control_plane: true
|
| 107 |
+
|
| 108 |
+
- agent_id: Response
|
| 109 |
+
domain: control_plane
|
| 110 |
+
capabilities: [synthesis, user_response]
|
| 111 |
+
languages: []
|
| 112 |
+
input_types: [conclusions, evidence_bundle]
|
| 113 |
+
output_types: [response]
|
| 114 |
+
required_evidence: []
|
| 115 |
+
permissions: [read_conclusions]
|
| 116 |
+
risk_class: medium
|
| 117 |
+
dependencies: []
|
| 118 |
+
execution_endpoint: agent://Response
|
| 119 |
+
control_plane: true
|
| 120 |
+
|
| 121 |
+
- agent_id: Quant
|
| 122 |
+
domain: markets
|
| 123 |
+
capabilities: [pricing, monte_carlo, greeks]
|
| 124 |
+
languages: [python]
|
| 125 |
+
input_types: [market_data, positions]
|
| 126 |
+
output_types: [price, risk_metrics]
|
| 127 |
+
required_evidence: [market_data]
|
| 128 |
+
permissions: [read_market_data]
|
| 129 |
+
risk_class: medium
|
| 130 |
+
dependencies: []
|
| 131 |
+
execution_endpoint: agent://Quant
|
| 132 |
+
control_plane: false
|
router/registry_facts.pl
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
% SPDX-License-Identifier: AGPL-3.0-only
|
| 2 |
+
% Copyright (C) 2026 David contributors
|
| 3 |
+
% GENERATED by scripts/gen_registry_facts.py from registry/seed_agents.yaml
|
| 4 |
+
% Do not edit by hand β regenerate via scripts/check_prolog_registry_sync.sh
|
| 5 |
+
%
|
| 6 |
+
% Agent atom map: registry agent_id β Prolog atom = downcase first letter
|
| 7 |
+
% LegacyCobol β legacyCobol, Risk β risk, Quant β quant
|
| 8 |
+
% Predicates: agent_id/2, agent/1, control_plane/1, capability/2,
|
| 9 |
+
% permission/2, risk_class/2, depends/2
|
| 10 |
+
|
| 11 |
+
agent_id('LegacyCobol', legacyCobol).
|
| 12 |
+
agent_id('Database', database).
|
| 13 |
+
agent_id('ReverseEngineering', reverseEngineering).
|
| 14 |
+
agent_id('Modernization', modernization).
|
| 15 |
+
agent_id('MigrationValidation', migrationValidation).
|
| 16 |
+
agent_id('Provenance', provenance).
|
| 17 |
+
agent_id('Risk', risk).
|
| 18 |
+
agent_id('Audit', audit).
|
| 19 |
+
agent_id('Response', response).
|
| 20 |
+
agent_id('Quant', quant).
|
| 21 |
+
|
| 22 |
+
agent(legacyCobol).
|
| 23 |
+
agent(database).
|
| 24 |
+
agent(reverseEngineering).
|
| 25 |
+
agent(modernization).
|
| 26 |
+
agent(migrationValidation).
|
| 27 |
+
agent(provenance).
|
| 28 |
+
agent(risk).
|
| 29 |
+
agent(audit).
|
| 30 |
+
agent(response).
|
| 31 |
+
agent(quant).
|
| 32 |
+
|
| 33 |
+
control_plane(risk).
|
| 34 |
+
control_plane(audit).
|
| 35 |
+
control_plane(response).
|
| 36 |
+
|
| 37 |
+
capability(legacyCobol, cobol).
|
| 38 |
+
capability(legacyCobol, copybook_analysis).
|
| 39 |
+
capability(legacyCobol, control_flow).
|
| 40 |
+
capability(legacyCobol, data_flow).
|
| 41 |
+
capability(legacyCobol, business_rule_extraction).
|
| 42 |
+
capability(legacyCobol, modernization_analysis).
|
| 43 |
+
capability(database, schema_analysis).
|
| 44 |
+
capability(database, db2).
|
| 45 |
+
capability(database, sql).
|
| 46 |
+
capability(reverseEngineering, call_graph).
|
| 47 |
+
capability(reverseEngineering, control_flow).
|
| 48 |
+
capability(reverseEngineering, data_flow).
|
| 49 |
+
capability(modernization, cobol_to_java).
|
| 50 |
+
capability(modernization, api_mapping).
|
| 51 |
+
capability(migrationValidation, equivalence).
|
| 52 |
+
capability(migrationValidation, behavioral_compare).
|
| 53 |
+
capability(provenance, provenance_dag).
|
| 54 |
+
capability(provenance, content_hash).
|
| 55 |
+
capability(provenance, authority).
|
| 56 |
+
capability(risk, risk_policy).
|
| 57 |
+
capability(risk, escalation).
|
| 58 |
+
capability(audit, audit_trail).
|
| 59 |
+
capability(audit, compliance).
|
| 60 |
+
capability(response, synthesis).
|
| 61 |
+
capability(response, user_response).
|
| 62 |
+
capability(quant, pricing).
|
| 63 |
+
capability(quant, monte_carlo).
|
| 64 |
+
capability(quant, greeks).
|
| 65 |
+
|
| 66 |
+
permission(legacyCobol, read_source).
|
| 67 |
+
permission(legacyCobol, read_copybooks).
|
| 68 |
+
permission(database, read_schema).
|
| 69 |
+
permission(reverseEngineering, read_source).
|
| 70 |
+
permission(modernization, read_source).
|
| 71 |
+
permission(modernization, write_plan).
|
| 72 |
+
permission(migrationValidation, read_outputs).
|
| 73 |
+
permission(provenance, read_evidence).
|
| 74 |
+
permission(provenance, write_provenance).
|
| 75 |
+
permission(risk, read_all_results).
|
| 76 |
+
permission(audit, read_all_results).
|
| 77 |
+
permission(response, read_conclusions).
|
| 78 |
+
permission(quant, read_market_data).
|
| 79 |
+
|
| 80 |
+
risk_class(legacyCobol, high).
|
| 81 |
+
risk_class(database, medium).
|
| 82 |
+
risk_class(reverseEngineering, medium).
|
| 83 |
+
risk_class(modernization, high).
|
| 84 |
+
risk_class(migrationValidation, high).
|
| 85 |
+
risk_class(provenance, medium).
|
| 86 |
+
risk_class(risk, critical).
|
| 87 |
+
risk_class(audit, high).
|
| 88 |
+
risk_class(response, medium).
|
| 89 |
+
risk_class(quant, medium).
|
| 90 |
+
|
| 91 |
+
depends(legacyCobol, database).
|
| 92 |
+
depends(legacyCobol, reverseEngineering).
|
| 93 |
+
depends(legacyCobol, provenance).
|
| 94 |
+
depends(modernization, legacyCobol).
|
| 95 |
+
depends(modernization, provenance).
|
| 96 |
+
depends(migrationValidation, provenance).
|
| 97 |
+
depends(migrationValidation, legacyCobol).
|
| 98 |
+
depends(audit, provenance).
|
router/scorer.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic sparse-MoA agent scoring (no LLM in the selection loop)."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
from typing import Iterable
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
WEIGHTS = {
|
| 10 |
+
"semantic_similarity": 1.0,
|
| 11 |
+
"capability_match": 1.2,
|
| 12 |
+
"input_match": 1.0,
|
| 13 |
+
"historical_success": 0.8,
|
| 14 |
+
"dependency_coverage": 0.6,
|
| 15 |
+
"risk_penalty": 0.9,
|
| 16 |
+
"permission_penalty": 1.5,
|
| 17 |
+
"unnecessary_agent_penalty": 0.7,
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
RISK_PENALTY = {"low": 0.0, "medium": 0.15, "high": 0.35, "critical": 0.55}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass(frozen=True)
|
| 24 |
+
class AgentDescriptor:
|
| 25 |
+
agent_id: str
|
| 26 |
+
domain: str
|
| 27 |
+
capabilities: frozenset[str]
|
| 28 |
+
input_types: frozenset[str]
|
| 29 |
+
permissions: frozenset[str]
|
| 30 |
+
risk_class: str
|
| 31 |
+
dependencies: frozenset[str]
|
| 32 |
+
historical_success: float = 0.0
|
| 33 |
+
control_plane: bool = False
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@dataclass(frozen=True)
|
| 37 |
+
class RequestContext:
|
| 38 |
+
required_capabilities: frozenset[str]
|
| 39 |
+
available_inputs: frozenset[str]
|
| 40 |
+
granted_permissions: frozenset[str]
|
| 41 |
+
max_risk_class: str = "high"
|
| 42 |
+
semantic_scores: dict[str, float] = field(default_factory=dict)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
_RISK_ORDER = ("low", "medium", "high", "critical")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _jaccard(a: frozenset[str], b: frozenset[str]) -> float:
|
| 49 |
+
if not a and not b:
|
| 50 |
+
return 1.0
|
| 51 |
+
if not a or not b:
|
| 52 |
+
return 0.0
|
| 53 |
+
return len(a & b) / len(a | b)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def risk_exceeds(agent_risk: str, max_risk: str) -> bool:
|
| 57 |
+
"""True if agent_risk is strictly above max_risk (low < medium < high < critical)."""
|
| 58 |
+
return _RISK_ORDER.index(agent_risk) > _RISK_ORDER.index(max_risk)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _risk_exceeds(agent_risk: str, max_risk: str) -> bool:
|
| 62 |
+
return risk_exceeds(agent_risk, max_risk)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def score_agent(agent: AgentDescriptor, req: RequestContext) -> float:
|
| 66 |
+
"""score = Ξ£ positive terms β penalties. Higher is better."""
|
| 67 |
+
sem = req.semantic_scores.get(agent.agent_id, 0.0)
|
| 68 |
+
cap = _jaccard(agent.capabilities, req.required_capabilities)
|
| 69 |
+
inp = _jaccard(agent.input_types, req.available_inputs)
|
| 70 |
+
hist = max(0.0, min(1.0, agent.historical_success))
|
| 71 |
+
# Dependency coverage: fraction of deps present in semantic top pool (caller may refine).
|
| 72 |
+
dep_cov = 1.0 if not agent.dependencies else 0.5
|
| 73 |
+
|
| 74 |
+
missing_perms = agent.permissions - req.granted_permissions
|
| 75 |
+
perm_pen = 1.0 if missing_perms else 0.0
|
| 76 |
+
risk_pen = RISK_PENALTY.get(agent.risk_class, 0.5)
|
| 77 |
+
if _risk_exceeds(agent.risk_class, req.max_risk_class):
|
| 78 |
+
risk_pen += 1.0
|
| 79 |
+
|
| 80 |
+
# Mild penalty if the agent shares no required capabilities (unnecessary).
|
| 81 |
+
unnecessary = 0.0 if cap > 0.0 or agent.control_plane else 1.0
|
| 82 |
+
|
| 83 |
+
return (
|
| 84 |
+
WEIGHTS["semantic_similarity"] * sem
|
| 85 |
+
+ WEIGHTS["capability_match"] * cap
|
| 86 |
+
+ WEIGHTS["input_match"] * inp
|
| 87 |
+
+ WEIGHTS["historical_success"] * hist
|
| 88 |
+
+ WEIGHTS["dependency_coverage"] * dep_cov
|
| 89 |
+
- WEIGHTS["risk_penalty"] * risk_pen
|
| 90 |
+
- WEIGHTS["permission_penalty"] * perm_pen
|
| 91 |
+
- WEIGHTS["unnecessary_agent_penalty"] * unnecessary
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def dependency_closure(
|
| 96 |
+
selected: Iterable[str],
|
| 97 |
+
registry: dict[str, AgentDescriptor],
|
| 98 |
+
) -> list[str]:
|
| 99 |
+
"""Expand selected agents with required dependencies (deterministic BFS)."""
|
| 100 |
+
out: list[str] = []
|
| 101 |
+
seen: set[str] = set()
|
| 102 |
+
queue = list(selected)
|
| 103 |
+
while queue:
|
| 104 |
+
aid = queue.pop(0)
|
| 105 |
+
if aid in seen:
|
| 106 |
+
continue
|
| 107 |
+
seen.add(aid)
|
| 108 |
+
out.append(aid)
|
| 109 |
+
agent = registry.get(aid)
|
| 110 |
+
if agent is None:
|
| 111 |
+
continue
|
| 112 |
+
for dep in sorted(agent.dependencies):
|
| 113 |
+
if dep not in seen:
|
| 114 |
+
queue.append(dep)
|
| 115 |
+
return out
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def select_minimum_set(
|
| 119 |
+
candidates: list[AgentDescriptor],
|
| 120 |
+
req: RequestContext,
|
| 121 |
+
registry: dict[str, AgentDescriptor],
|
| 122 |
+
top_k: int = 20,
|
| 123 |
+
include_control_plane: bool = True,
|
| 124 |
+
) -> list[str]:
|
| 125 |
+
"""
|
| 126 |
+
Pipeline: score β sort deterministically β top-K β dependency closure
|
| 127 |
+
β optional control-plane agents β return agent_ids.
|
| 128 |
+
"""
|
| 129 |
+
ranked = sorted(
|
| 130 |
+
candidates,
|
| 131 |
+
key=lambda a: (-score_agent(a, req), a.agent_id),
|
| 132 |
+
)[:top_k]
|
| 133 |
+
|
| 134 |
+
# Keep agents that match at least one required capability (or control plane later).
|
| 135 |
+
core = [
|
| 136 |
+
a.agent_id
|
| 137 |
+
for a in ranked
|
| 138 |
+
if a.capabilities & req.required_capabilities
|
| 139 |
+
]
|
| 140 |
+
closed = dependency_closure(core, registry)
|
| 141 |
+
|
| 142 |
+
if include_control_plane:
|
| 143 |
+
for a in candidates:
|
| 144 |
+
if a.control_plane and a.agent_id not in closed:
|
| 145 |
+
closed.append(a.agent_id)
|
| 146 |
+
|
| 147 |
+
return closed
|
router/symbolic.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Symbolic hard filters and minimum-agent constraint set (Spec / Hilbert).
|
| 2 |
+
|
| 3 |
+
Hard filters are predicates (True/False), not soft score penalties.
|
| 4 |
+
Scorer remains source of truth for scores β import score_agent / dependency_closure;
|
| 5 |
+
never redefine WEIGHTS or score_agent.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from typing import Iterable, Sequence
|
| 11 |
+
|
| 12 |
+
from scorer import (
|
| 13 |
+
AgentDescriptor,
|
| 14 |
+
RequestContext,
|
| 15 |
+
dependency_closure,
|
| 16 |
+
risk_exceeds,
|
| 17 |
+
score_agent,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def capability_eligible(agent: AgentDescriptor, req: RequestContext) -> bool:
|
| 22 |
+
"""Agent shares at least one required capability, or is control-plane."""
|
| 23 |
+
if agent.control_plane:
|
| 24 |
+
return True
|
| 25 |
+
return bool(agent.capabilities & req.required_capabilities)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def permission_eligible(agent: AgentDescriptor, req: RequestContext) -> bool:
|
| 29 |
+
"""Agent permissions must be a subset of granted permissions."""
|
| 30 |
+
return agent.permissions <= req.granted_permissions
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def risk_eligible(agent: AgentDescriptor, req: RequestContext) -> bool:
|
| 34 |
+
"""Agent risk_class must not exceed req.max_risk_class."""
|
| 35 |
+
return not risk_exceeds(agent.risk_class, req.max_risk_class)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def apply_symbolic_filters(
|
| 39 |
+
candidates: Iterable[AgentDescriptor],
|
| 40 |
+
req: RequestContext,
|
| 41 |
+
) -> list[AgentDescriptor]:
|
| 42 |
+
"""Return agents that pass capability, permission, and risk hard filters."""
|
| 43 |
+
return [
|
| 44 |
+
a
|
| 45 |
+
for a in candidates
|
| 46 |
+
if capability_eligible(a, req)
|
| 47 |
+
and permission_eligible(a, req)
|
| 48 |
+
and risk_eligible(a, req)
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _greedy_cover(
|
| 53 |
+
eligible: Sequence[AgentDescriptor],
|
| 54 |
+
req: RequestContext,
|
| 55 |
+
) -> list[str]:
|
| 56 |
+
"""Greedy set-cover over required_capabilities (deterministic)."""
|
| 57 |
+
uncovered = set(req.required_capabilities)
|
| 58 |
+
remaining = list(eligible)
|
| 59 |
+
picked: list[str] = []
|
| 60 |
+
scores = {a.agent_id: score_agent(a, req) for a in remaining}
|
| 61 |
+
|
| 62 |
+
while uncovered and remaining:
|
| 63 |
+
# Primary: most still-uncovered caps; secondary: highest score; tertiary: agent_id
|
| 64 |
+
def key(a: AgentDescriptor) -> tuple:
|
| 65 |
+
cover = len(a.capabilities & uncovered)
|
| 66 |
+
return (-cover, -scores[a.agent_id], a.agent_id)
|
| 67 |
+
|
| 68 |
+
best = min(remaining, key=key)
|
| 69 |
+
cover = len(best.capabilities & uncovered)
|
| 70 |
+
if cover == 0:
|
| 71 |
+
break
|
| 72 |
+
picked.append(best.agent_id)
|
| 73 |
+
uncovered -= best.capabilities
|
| 74 |
+
remaining = [a for a in remaining if a.agent_id != best.agent_id]
|
| 75 |
+
|
| 76 |
+
return picked
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _closure_passes_hard_filters(
|
| 80 |
+
agent_id: str,
|
| 81 |
+
req: RequestContext,
|
| 82 |
+
registry: dict[str, AgentDescriptor],
|
| 83 |
+
) -> bool:
|
| 84 |
+
"""True iff agent_id and all transitive deps pass permission + risk.
|
| 85 |
+
|
| 86 |
+
capability_eligible is waived for pure dependency agents pulled in solely
|
| 87 |
+
as deps (they need not intersect required_capabilities).
|
| 88 |
+
"""
|
| 89 |
+
for cid in dependency_closure([agent_id], registry):
|
| 90 |
+
agent = registry.get(cid)
|
| 91 |
+
if agent is None:
|
| 92 |
+
return False
|
| 93 |
+
if not permission_eligible(agent, req):
|
| 94 |
+
return False
|
| 95 |
+
if not risk_eligible(agent, req):
|
| 96 |
+
return False
|
| 97 |
+
return True
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def minimum_agent_set(
|
| 101 |
+
candidates: list[AgentDescriptor],
|
| 102 |
+
req: RequestContext,
|
| 103 |
+
registry: dict[str, AgentDescriptor],
|
| 104 |
+
top_k: int = 20,
|
| 105 |
+
mandatory_control_plane: Sequence[str] = ("Risk", "Response"),
|
| 106 |
+
) -> list[str]:
|
| 107 |
+
"""
|
| 108 |
+
Hard-filter + greedy cover + dependency closure + mandatory control plane.
|
| 109 |
+
|
| 110 |
+
Order: greedy picks, then closure BFS order, then mandatory control plane
|
| 111 |
+
in the given order. Unique and stable.
|
| 112 |
+
|
| 113 |
+
After closure, every agent must pass permission_eligible and risk_eligible.
|
| 114 |
+
capability_eligible is waived for pure dependency agents. If a dependency
|
| 115 |
+
fails those hard filters, its parent is ineligible β that parent and its
|
| 116 |
+
incomplete closure are dropped.
|
| 117 |
+
|
| 118 |
+
Mandatory control-plane agents skip the risk filter (matching scorer
|
| 119 |
+
include_control_plane) but still require permission_eligible.
|
| 120 |
+
"""
|
| 121 |
+
# 1β2. Score, sort deterministically, take top_k
|
| 122 |
+
ranked = sorted(
|
| 123 |
+
candidates,
|
| 124 |
+
key=lambda a: (-score_agent(a, req), a.agent_id),
|
| 125 |
+
)[:top_k]
|
| 126 |
+
|
| 127 |
+
# 3. Hard filters on the pool
|
| 128 |
+
eligible = apply_symbolic_filters(ranked, req)
|
| 129 |
+
|
| 130 |
+
# 4. Greedy set-cover
|
| 131 |
+
greedy_ids = _greedy_cover(eligible, req)
|
| 132 |
+
|
| 133 |
+
# 5β6. Closure per pick; drop parents whose closure fails perm/risk
|
| 134 |
+
kept: list[str] = []
|
| 135 |
+
for aid in greedy_ids:
|
| 136 |
+
if _closure_passes_hard_filters(aid, req, registry):
|
| 137 |
+
kept.append(aid)
|
| 138 |
+
|
| 139 |
+
# Closure BFS over surviving greedy picks (full registry)
|
| 140 |
+
result = dependency_closure(kept, registry)
|
| 141 |
+
|
| 142 |
+
# 7. Mandatory control plane: permission only (skip risk), like include_control_plane
|
| 143 |
+
for mid in mandatory_control_plane:
|
| 144 |
+
if mid in result:
|
| 145 |
+
continue
|
| 146 |
+
agent = registry.get(mid)
|
| 147 |
+
if agent is None:
|
| 148 |
+
continue
|
| 149 |
+
if permission_eligible(agent, req):
|
| 150 |
+
result.append(mid)
|
| 151 |
+
|
| 152 |
+
# 8. Already unique via dependency_closure + append checks
|
| 153 |
+
return result
|
router/symbolic_rules.pl
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
% SPDX-License-Identifier: AGPL-3.0-only
|
| 2 |
+
% Copyright (C) 2026 David contributors
|
| 3 |
+
% Executable eligibility rules for Synthetic David sparse router.
|
| 4 |
+
% Authoritative symbolic layer: embeddings propose; these rules decide.
|
| 5 |
+
% Agent facts live in registry_facts.pl (generated from seed_agents.yaml).
|
| 6 |
+
% Atom map: registry agent_id β Prolog atom via downcase-first-letter
|
| 7 |
+
% (see agent_id/2 in registry_facts.pl). Quant is a negative agent.
|
| 8 |
+
% Run: swipl -q -s symbolic_rules.pl -g smoke_cobol -t halt
|
| 9 |
+
% Product path: J writes request + ann_candidate facts, calls run_eligibility/1.
|
| 10 |
+
|
| 11 |
+
:- style_check(-singleton).
|
| 12 |
+
:- include('registry_facts.pl').
|
| 13 |
+
|
| 14 |
+
% ---- risk order ----
|
| 15 |
+
risk_rank(low, 0).
|
| 16 |
+
risk_rank(medium, 1).
|
| 17 |
+
risk_rank(high, 2).
|
| 18 |
+
risk_rank(critical, 3).
|
| 19 |
+
|
| 20 |
+
risk_exceeds(AgentRisk, MaxRisk) :-
|
| 21 |
+
risk_rank(AgentRisk, A),
|
| 22 |
+
risk_rank(MaxRisk, M),
|
| 23 |
+
A > M.
|
| 24 |
+
|
| 25 |
+
% ---- request context (dynamic; J / smoke asserts) ----
|
| 26 |
+
% required_capability(Req, Cap).
|
| 27 |
+
% granted_permission(Req, Perm).
|
| 28 |
+
% max_risk_class(Req, Class).
|
| 29 |
+
% ann_candidate(Req, AgentAtom). % optional; when present, core pick restricted
|
| 30 |
+
|
| 31 |
+
:- dynamic required_capability/2, granted_permission/2, max_risk_class/2.
|
| 32 |
+
:- dynamic ann_candidate/2.
|
| 33 |
+
|
| 34 |
+
% ---- hard filters ----
|
| 35 |
+
capability_eligible(Agent, Req) :-
|
| 36 |
+
control_plane(Agent),
|
| 37 |
+
agent(Agent).
|
| 38 |
+
capability_eligible(Agent, Req) :-
|
| 39 |
+
agent(Agent),
|
| 40 |
+
capability(Agent, Cap),
|
| 41 |
+
required_capability(Req, Cap).
|
| 42 |
+
|
| 43 |
+
permission_eligible(Agent, Req) :-
|
| 44 |
+
agent(Agent),
|
| 45 |
+
forall(permission(Agent, P), granted_permission(Req, P)).
|
| 46 |
+
|
| 47 |
+
risk_eligible(Agent, Req) :-
|
| 48 |
+
agent(Agent),
|
| 49 |
+
risk_class(Agent, AR),
|
| 50 |
+
max_risk_class(Req, MR),
|
| 51 |
+
\+ risk_exceeds(AR, MR).
|
| 52 |
+
|
| 53 |
+
% Core eligible: all three hard filters (control-plane still needs risk unless mandatory).
|
| 54 |
+
eligible(Agent, Req) :-
|
| 55 |
+
capability_eligible(Agent, Req),
|
| 56 |
+
permission_eligible(Agent, Req),
|
| 57 |
+
risk_eligible(Agent, Req).
|
| 58 |
+
|
| 59 |
+
% Dependency agents may skip capability intersection; still need permission + risk.
|
| 60 |
+
dep_eligible(Agent, Req) :-
|
| 61 |
+
agent(Agent),
|
| 62 |
+
permission_eligible(Agent, Req),
|
| 63 |
+
risk_eligible(Agent, Req).
|
| 64 |
+
|
| 65 |
+
% Mandatory control plane: permission only (matches scaffold; Risk may be critical).
|
| 66 |
+
mandatory_control_eligible(Agent, Req) :-
|
| 67 |
+
control_plane(Agent),
|
| 68 |
+
permission_eligible(Agent, Req).
|
| 69 |
+
|
| 70 |
+
% ---- dependency closure (BFS via setof/findall) ----
|
| 71 |
+
closure(Seed, Closed) :-
|
| 72 |
+
closure_(Seed, [], Closed).
|
| 73 |
+
|
| 74 |
+
closure_([], Acc, Closed) :-
|
| 75 |
+
reverse(Acc, Closed).
|
| 76 |
+
closure_([H|T], Acc, Closed) :-
|
| 77 |
+
memberchk(H, Acc), !,
|
| 78 |
+
closure_(T, Acc, Closed).
|
| 79 |
+
closure_([H|T], Acc, Closed) :-
|
| 80 |
+
findall(D, depends(H, D), Deps),
|
| 81 |
+
append(T, Deps, Queue),
|
| 82 |
+
closure_(Queue, [H|Acc], Closed).
|
| 83 |
+
|
| 84 |
+
% ---- minimum agent set ----
|
| 85 |
+
% 1. Collect agents that cover at least one required capability and pass hard filters
|
| 86 |
+
% (if ann_candidate/2 facts exist for Req, restrict core seeds to those candidates)
|
| 87 |
+
% 2. Greedy cover of required capabilities (order by agent atom for determinism)
|
| 88 |
+
% 3. Dependency closure; drop incomplete parents if a dep fails dep_eligible
|
| 89 |
+
% 4. Append mandatory Risk + Response if permitted
|
| 90 |
+
% Quant has no overlapping caps for COBOL requests β dropped.
|
| 91 |
+
|
| 92 |
+
covers_required(Agent, Req, Cap) :-
|
| 93 |
+
required_capability(Req, Cap),
|
| 94 |
+
capability(Agent, Cap).
|
| 95 |
+
|
| 96 |
+
% When ANN candidates are asserted for Req, only those may be core seeds.
|
| 97 |
+
% Deps and mandatory control-plane agents are still closed in below.
|
| 98 |
+
in_ann_or_unrestricted(Agent, Req) :-
|
| 99 |
+
( ann_candidate(Req, _) -> ann_candidate(Req, Agent) ; true ).
|
| 100 |
+
|
| 101 |
+
candidate_core(Agent, Req) :-
|
| 102 |
+
eligible(Agent, Req),
|
| 103 |
+
\+ control_plane(Agent),
|
| 104 |
+
covers_required(Agent, Req, _),
|
| 105 |
+
in_ann_or_unrestricted(Agent, Req).
|
| 106 |
+
|
| 107 |
+
greedy_cover(Req, Selected) :-
|
| 108 |
+
findall(Cap, required_capability(Req, Cap), Caps0),
|
| 109 |
+
sort(Caps0, Caps),
|
| 110 |
+
findall(A, candidate_core(A, Req), Cands0),
|
| 111 |
+
sort(Cands0, Cands),
|
| 112 |
+
greedy_cover_(Caps, Cands, Req, [], Selected).
|
| 113 |
+
|
| 114 |
+
greedy_cover_([], _, _, Acc, Selected) :-
|
| 115 |
+
reverse(Acc, Selected).
|
| 116 |
+
greedy_cover_(Uncovered, Cands, Req, Acc, Selected) :-
|
| 117 |
+
Uncovered \= [],
|
| 118 |
+
best_cover(Uncovered, Cands, Acc, Best, CoverN),
|
| 119 |
+
CoverN > 0, !,
|
| 120 |
+
findall(C, (member(C, Uncovered), \+ capability(Best, C)), Rest),
|
| 121 |
+
greedy_cover_(Rest, Cands, Req, [Best|Acc], Selected).
|
| 122 |
+
greedy_cover_(_, _, _, Acc, Selected) :-
|
| 123 |
+
reverse(Acc, Selected).
|
| 124 |
+
|
| 125 |
+
best_cover(Uncovered, Cands, Already, Best, CoverN) :-
|
| 126 |
+
findall(N-A, (
|
| 127 |
+
member(A, Cands),
|
| 128 |
+
\+ memberchk(A, Already),
|
| 129 |
+
findall(C, (member(C, Uncovered), capability(A, C)), Cs),
|
| 130 |
+
length(Cs, N),
|
| 131 |
+
N > 0
|
| 132 |
+
), Pairs0),
|
| 133 |
+
sort(0, @>=, Pairs0, Sorted),
|
| 134 |
+
Sorted = [CoverN-Best|_].
|
| 135 |
+
|
| 136 |
+
filter_closure([], _, []).
|
| 137 |
+
filter_closure([A|As], Req, [A|Bs]) :-
|
| 138 |
+
( memberchk(A, As) -> fail ; true ),
|
| 139 |
+
( candidate_core(A, Req) -> true
|
| 140 |
+
; depends_from_selected(A, [A|As]) -> dep_eligible(A, Req)
|
| 141 |
+
; dep_eligible(A, Req)
|
| 142 |
+
), !,
|
| 143 |
+
filter_closure(As, Req, Bs).
|
| 144 |
+
filter_closure([_|As], Req, Bs) :-
|
| 145 |
+
filter_closure(As, Req, Bs).
|
| 146 |
+
|
| 147 |
+
closed_ok(Closed, Req, Seed) :-
|
| 148 |
+
forall(member(A, Seed), candidate_core(A, Req)),
|
| 149 |
+
forall(member(A, Closed), dep_eligible(A, Req)).
|
| 150 |
+
|
| 151 |
+
minimum_agent_set(Req, Final) :-
|
| 152 |
+
greedy_cover(Req, Seed),
|
| 153 |
+
closure(Seed, Closed0),
|
| 154 |
+
include(dep_ok(Req), Closed0, Closed),
|
| 155 |
+
include(deps_satisfied(Closed), Closed, CoreKept),
|
| 156 |
+
mandatory_control(Req, Mand),
|
| 157 |
+
append(CoreKept, Mand, Raw),
|
| 158 |
+
list_to_set(Raw, Final).
|
| 159 |
+
|
| 160 |
+
dep_ok(Req, Agent) :- dep_eligible(Agent, Req).
|
| 161 |
+
|
| 162 |
+
deps_satisfied(Closed, Agent) :-
|
| 163 |
+
forall(depends(Agent, D), memberchk(D, Closed)).
|
| 164 |
+
|
| 165 |
+
mandatory_control(Req, Mand) :-
|
| 166 |
+
findall(A, (
|
| 167 |
+
member(A, [risk, response]),
|
| 168 |
+
mandatory_control_eligible(A, Req)
|
| 169 |
+
), Mand).
|
| 170 |
+
|
| 171 |
+
% ---- product entry: print selected atoms (J parses this line) ----
|
| 172 |
+
run_eligibility(Req) :-
|
| 173 |
+
minimum_agent_set(Req, Selected),
|
| 174 |
+
format('selected:~w~n', [Selected]).
|
| 175 |
+
|
| 176 |
+
% ---- COBOL smoke request (no ANN candidates β full registry core) ----
|
| 177 |
+
assert_cobol_req :-
|
| 178 |
+
retractall(required_capability(cobol_req, _)),
|
| 179 |
+
retractall(granted_permission(cobol_req, _)),
|
| 180 |
+
retractall(max_risk_class(cobol_req, _)),
|
| 181 |
+
retractall(ann_candidate(cobol_req, _)),
|
| 182 |
+
assertz(required_capability(cobol_req, cobol)),
|
| 183 |
+
assertz(required_capability(cobol_req, business_rule_extraction)),
|
| 184 |
+
assertz(granted_permission(cobol_req, read_source)),
|
| 185 |
+
assertz(granted_permission(cobol_req, read_copybooks)),
|
| 186 |
+
assertz(granted_permission(cobol_req, read_schema)),
|
| 187 |
+
assertz(granted_permission(cobol_req, read_evidence)),
|
| 188 |
+
assertz(granted_permission(cobol_req, write_provenance)),
|
| 189 |
+
assertz(granted_permission(cobol_req, read_all_results)),
|
| 190 |
+
assertz(granted_permission(cobol_req, read_conclusions)),
|
| 191 |
+
assertz(max_risk_class(cobol_req, high)).
|
| 192 |
+
|
| 193 |
+
smoke_cobol :-
|
| 194 |
+
assert_cobol_req,
|
| 195 |
+
minimum_agent_set(cobol_req, Selected),
|
| 196 |
+
format('selected:~w~n', [Selected]),
|
| 197 |
+
( memberchk(legacyCobol, Selected) -> true ; format('FAIL missing LegacyCobol~n'), halt(1) ),
|
| 198 |
+
( memberchk(database, Selected) -> true ; format('FAIL missing Database~n'), halt(1) ),
|
| 199 |
+
( memberchk(provenance, Selected) -> true ; format('FAIL missing Provenance~n'), halt(1) ),
|
| 200 |
+
( memberchk(quant, Selected) -> format('FAIL Quant present~n'), halt(1) ; true ),
|
| 201 |
+
( memberchk(risk, Selected) -> true ; format('FAIL missing Risk~n'), halt(1) ),
|
| 202 |
+
( memberchk(response, Selected) -> true ; format('FAIL missing Response~n'), halt(1) ),
|
| 203 |
+
format('smoke_cobol: OK~n').
|
router/test_scorer.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Smoke tests for deterministic sparse selection."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import unittest
|
| 6 |
+
|
| 7 |
+
from scorer import AgentDescriptor, RequestContext, score_agent, select_minimum_set
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _agent(aid, caps, deps=(), control=False, risk="medium", perms=(), inputs=()):
|
| 11 |
+
return AgentDescriptor(
|
| 12 |
+
agent_id=aid,
|
| 13 |
+
domain="test",
|
| 14 |
+
capabilities=frozenset(caps),
|
| 15 |
+
input_types=frozenset(inputs or ("source_code",)),
|
| 16 |
+
permissions=frozenset(perms or ("read_source",)),
|
| 17 |
+
risk_class=risk,
|
| 18 |
+
dependencies=frozenset(deps),
|
| 19 |
+
historical_success=0.5,
|
| 20 |
+
control_plane=control,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class ScorerTests(unittest.TestCase):
|
| 25 |
+
def setUp(self):
|
| 26 |
+
self.registry = {
|
| 27 |
+
"LegacyCobol": _agent(
|
| 28 |
+
"LegacyCobol",
|
| 29 |
+
["cobol", "business_rule_extraction"],
|
| 30 |
+
deps=("Database", "Provenance"),
|
| 31 |
+
risk="high",
|
| 32 |
+
),
|
| 33 |
+
"Database": _agent("Database", ["schema_analysis"], inputs=("db_schema",)),
|
| 34 |
+
"Provenance": _agent("Provenance", ["provenance_dag"]),
|
| 35 |
+
"Quant": _agent("Quant", ["pricing", "monte_carlo"]),
|
| 36 |
+
"Risk": _agent("Risk", ["risk_policy"], control=True, risk="critical"),
|
| 37 |
+
"Response": _agent("Response", ["synthesis"], control=True),
|
| 38 |
+
}
|
| 39 |
+
self.candidates = list(self.registry.values())
|
| 40 |
+
self.req = RequestContext(
|
| 41 |
+
required_capabilities=frozenset({"cobol", "business_rule_extraction"}),
|
| 42 |
+
available_inputs=frozenset({"source_code", "copybooks"}),
|
| 43 |
+
granted_permissions=frozenset({"read_source", "read_copybooks"}),
|
| 44 |
+
max_risk_class="high",
|
| 45 |
+
semantic_scores={
|
| 46 |
+
"LegacyCobol": 0.92,
|
| 47 |
+
"Database": 0.4,
|
| 48 |
+
"Quant": 0.1,
|
| 49 |
+
"Provenance": 0.3,
|
| 50 |
+
"Risk": 0.2,
|
| 51 |
+
"Response": 0.2,
|
| 52 |
+
},
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
def test_cobol_query_excludes_quant(self):
|
| 56 |
+
selected = select_minimum_set(self.candidates, self.req, self.registry)
|
| 57 |
+
self.assertIn("LegacyCobol", selected)
|
| 58 |
+
self.assertIn("Database", selected)
|
| 59 |
+
self.assertIn("Provenance", selected)
|
| 60 |
+
self.assertNotIn("Quant", selected)
|
| 61 |
+
self.assertIn("Risk", selected)
|
| 62 |
+
self.assertIn("Response", selected)
|
| 63 |
+
|
| 64 |
+
def test_score_orders_legacy_above_quant(self):
|
| 65 |
+
legacy = score_agent(self.registry["LegacyCobol"], self.req)
|
| 66 |
+
quant = score_agent(self.registry["Quant"], self.req)
|
| 67 |
+
self.assertGreater(legacy, quant)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
if __name__ == "__main__":
|
| 71 |
+
unittest.main()
|
router/test_symbolic.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Smoke tests for symbolic hard filters + minimum_agent_set."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import unittest
|
| 6 |
+
|
| 7 |
+
from scorer import AgentDescriptor, RequestContext
|
| 8 |
+
from symbolic import (
|
| 9 |
+
apply_symbolic_filters,
|
| 10 |
+
capability_eligible,
|
| 11 |
+
minimum_agent_set,
|
| 12 |
+
permission_eligible,
|
| 13 |
+
risk_eligible,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _agent(aid, caps, deps=(), control=False, risk="medium", perms=(), inputs=()):
|
| 18 |
+
return AgentDescriptor(
|
| 19 |
+
agent_id=aid,
|
| 20 |
+
domain="test",
|
| 21 |
+
capabilities=frozenset(caps),
|
| 22 |
+
input_types=frozenset(inputs or ("source_code",)),
|
| 23 |
+
permissions=frozenset(perms or ("read_source",)),
|
| 24 |
+
risk_class=risk,
|
| 25 |
+
dependencies=frozenset(deps),
|
| 26 |
+
historical_success=0.5,
|
| 27 |
+
control_plane=control,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class SymbolicTests(unittest.TestCase):
|
| 32 |
+
def setUp(self):
|
| 33 |
+
self.registry = {
|
| 34 |
+
"LegacyCobol": _agent(
|
| 35 |
+
"LegacyCobol",
|
| 36 |
+
["cobol", "business_rule_extraction"],
|
| 37 |
+
deps=("Database", "Provenance"),
|
| 38 |
+
risk="high",
|
| 39 |
+
),
|
| 40 |
+
"Database": _agent("Database", ["schema_analysis"], inputs=("db_schema",)),
|
| 41 |
+
"Provenance": _agent("Provenance", ["provenance_dag"]),
|
| 42 |
+
"Quant": _agent("Quant", ["pricing", "monte_carlo"]),
|
| 43 |
+
"Risk": _agent("Risk", ["risk_policy"], control=True, risk="critical"),
|
| 44 |
+
"Response": _agent("Response", ["synthesis"], control=True),
|
| 45 |
+
}
|
| 46 |
+
self.candidates = list(self.registry.values())
|
| 47 |
+
self.req = RequestContext(
|
| 48 |
+
required_capabilities=frozenset({"cobol", "business_rule_extraction"}),
|
| 49 |
+
available_inputs=frozenset({"source_code", "copybooks"}),
|
| 50 |
+
granted_permissions=frozenset({"read_source", "read_copybooks"}),
|
| 51 |
+
max_risk_class="high",
|
| 52 |
+
semantic_scores={
|
| 53 |
+
"LegacyCobol": 0.92,
|
| 54 |
+
"Database": 0.4,
|
| 55 |
+
"Quant": 0.1,
|
| 56 |
+
"Provenance": 0.3,
|
| 57 |
+
"Risk": 0.2,
|
| 58 |
+
"Response": 0.2,
|
| 59 |
+
},
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
def test_cobol_query_minimum_set(self):
|
| 63 |
+
selected = minimum_agent_set(self.candidates, self.req, self.registry)
|
| 64 |
+
self.assertIn("LegacyCobol", selected)
|
| 65 |
+
self.assertIn("Database", selected)
|
| 66 |
+
self.assertIn("Provenance", selected)
|
| 67 |
+
self.assertNotIn("Quant", selected)
|
| 68 |
+
self.assertIn("Risk", selected)
|
| 69 |
+
self.assertIn("Response", selected)
|
| 70 |
+
|
| 71 |
+
def test_quant_never_selected_for_cobol(self):
|
| 72 |
+
selected = minimum_agent_set(self.candidates, self.req, self.registry)
|
| 73 |
+
self.assertNotIn("Quant", selected)
|
| 74 |
+
self.assertFalse(capability_eligible(self.registry["Quant"], self.req))
|
| 75 |
+
|
| 76 |
+
def test_permission_filter_drops_ungranted(self):
|
| 77 |
+
sneaky = _agent(
|
| 78 |
+
"SneakyWrite",
|
| 79 |
+
["cobol"],
|
| 80 |
+
perms=("read_source", "write_prod"),
|
| 81 |
+
)
|
| 82 |
+
req = RequestContext(
|
| 83 |
+
required_capabilities=frozenset({"cobol"}),
|
| 84 |
+
available_inputs=frozenset({"source_code"}),
|
| 85 |
+
granted_permissions=frozenset({"read_source"}),
|
| 86 |
+
max_risk_class="high",
|
| 87 |
+
semantic_scores={"SneakyWrite": 0.99},
|
| 88 |
+
)
|
| 89 |
+
self.assertFalse(permission_eligible(sneaky, req))
|
| 90 |
+
filtered = apply_symbolic_filters([sneaky], req)
|
| 91 |
+
self.assertEqual(filtered, [])
|
| 92 |
+
selected = minimum_agent_set([sneaky], req, {"SneakyWrite": sneaky})
|
| 93 |
+
self.assertNotIn("SneakyWrite", selected)
|
| 94 |
+
|
| 95 |
+
def test_risk_filter_drops_critical_when_max_medium(self):
|
| 96 |
+
critical = _agent("HotPath", ["cobol"], risk="critical")
|
| 97 |
+
req = RequestContext(
|
| 98 |
+
required_capabilities=frozenset({"cobol"}),
|
| 99 |
+
available_inputs=frozenset({"source_code"}),
|
| 100 |
+
granted_permissions=frozenset({"read_source"}),
|
| 101 |
+
max_risk_class="medium",
|
| 102 |
+
semantic_scores={"HotPath": 0.99, "Risk": 0.2, "Response": 0.2},
|
| 103 |
+
)
|
| 104 |
+
self.assertFalse(risk_eligible(critical, req))
|
| 105 |
+
filtered = apply_symbolic_filters([critical], req)
|
| 106 |
+
self.assertEqual(filtered, [])
|
| 107 |
+
|
| 108 |
+
# Mandatory control plane still included despite Risk being critical
|
| 109 |
+
# (risk gate skipped for mandatory CP; permission still required).
|
| 110 |
+
registry = {
|
| 111 |
+
"HotPath": critical,
|
| 112 |
+
"Risk": self.registry["Risk"],
|
| 113 |
+
"Response": self.registry["Response"],
|
| 114 |
+
}
|
| 115 |
+
selected = minimum_agent_set(
|
| 116 |
+
list(registry.values()), req, registry
|
| 117 |
+
)
|
| 118 |
+
self.assertNotIn("HotPath", selected)
|
| 119 |
+
self.assertIn("Risk", selected)
|
| 120 |
+
self.assertIn("Response", selected)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
if __name__ == "__main__":
|
| 124 |
+
unittest.main()
|
scripts/README_agent_ann.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Live AGENT-index Postgres smoke
|
| 2 |
+
|
| 3 |
+
Product indexes: `sql/001_agent_and_evidence_indexes.sql` (pgvector).
|
| 4 |
+
|
| 5 |
+
Harness (Python, not product):
|
| 6 |
+
- `load_agent_registry.py` β `registry/seed_agents.yaml` β `agent_registry`, deterministic hashed bag-of-tokens β `vector(1536)`
|
| 7 |
+
- `agent_ann_smoke.py` β COBOL balance query ANN top-K (`<=>` cosine)
|
| 8 |
+
|
| 9 |
+
Done criteria: LegacyCobol in top-K; Quant not preferred before symbolic filter.
|
| 10 |
+
|
| 11 |
+
Measured log: `logs/agent_ann_smoke.log`
|
scripts/README_evidence_hybrid.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Live EVIDENCE-index Postgres hybrid smoke
|
| 2 |
+
|
| 3 |
+
Product indexes / retrieve: `sql/001_agent_and_evidence_indexes.sql`,
|
| 4 |
+
`sql/002_evidence_hybrid_retrieve.sql`, fixtures in `sql/003_evidence_seed_cobol.sql`
|
| 5 |
+
(embedding left NULL in 003 β loader fills vectors).
|
| 6 |
+
|
| 7 |
+
Harness (Python, not product):
|
| 8 |
+
- `load_evidence_seed.py` β same fixture IDs as 003 β `evidence`, deterministic hashed
|
| 9 |
+
bag-of-tokens β `vector(1536)` via `embed` / `fmt_vector` from `load_agent_registry.py`
|
| 10 |
+
- `evidence_hybrid_smoke.py` β COBOL balance hybrid query (dense + lexical + authority;
|
| 11 |
+
`min_authority=0.5`; `require_parent=false` β junk dropped by authority floor)
|
| 12 |
+
|
| 13 |
+
Done criteria: `ev_copybook_01` / `ev_cobol_src_01` / `ev_db2_schema_01` beat Quant;
|
| 14 |
+
`ev_junk_low_auth` dropped at `min_authority=0.5`.
|
| 15 |
+
|
| 16 |
+
Measured log: `logs/evidence_hybrid_smoke.log`
|
scripts/agent_ann_smoke.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Harness: embed COBOL balance query, ANN top-K against agent_registry."""
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
# reuse embed from loader
|
| 10 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 11 |
+
from load_agent_registry import embed, fmt_vector
|
| 12 |
+
|
| 13 |
+
import psycopg2
|
| 14 |
+
|
| 15 |
+
QUERY = (
|
| 16 |
+
"Explain how this COBOL payment program calculates the account balance "
|
| 17 |
+
"and determine whether its Java replacement preserves the behavior. "
|
| 18 |
+
"cobol copybook business_rule_extraction control_flow data_flow "
|
| 19 |
+
"legacy_mainframe modernization_analysis"
|
| 20 |
+
)
|
| 21 |
+
TOP_K = 5
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def main() -> int:
|
| 25 |
+
dsn = sys.argv[1] if len(sys.argv) > 1 else "dbname=david user=david host=127.0.0.1"
|
| 26 |
+
qvec = fmt_vector(embed(QUERY))
|
| 27 |
+
conn = psycopg2.connect(dsn)
|
| 28 |
+
with conn.cursor() as cur:
|
| 29 |
+
cur.execute(
|
| 30 |
+
"""
|
| 31 |
+
SELECT agent_id, domain, risk_class,
|
| 32 |
+
1 - (embedding <=> %s::vector) AS cosine_sim
|
| 33 |
+
FROM agent_registry
|
| 34 |
+
WHERE embedding IS NOT NULL
|
| 35 |
+
ORDER BY embedding <=> %s::vector
|
| 36 |
+
LIMIT %s
|
| 37 |
+
""",
|
| 38 |
+
(qvec, qvec, TOP_K),
|
| 39 |
+
)
|
| 40 |
+
rows = cur.fetchall()
|
| 41 |
+
conn.close()
|
| 42 |
+
|
| 43 |
+
print(f"query={QUERY[:80]}...")
|
| 44 |
+
print(f"top_k={TOP_K}")
|
| 45 |
+
for i, (aid, domain, risk, sim) in enumerate(rows, 1):
|
| 46 |
+
print(f"{i}\t{aid}\t{domain}\t{risk}\tsim={sim:.6f}")
|
| 47 |
+
|
| 48 |
+
ids = [r[0] for r in rows]
|
| 49 |
+
ok_legacy = "LegacyCobol" in ids
|
| 50 |
+
# Quant should not outrank LegacyCobol (prefer: Legacy ahead of Quant if both present)
|
| 51 |
+
if "Quant" in ids and "LegacyCobol" in ids:
|
| 52 |
+
ok_order = ids.index("LegacyCobol") < ids.index("Quant")
|
| 53 |
+
else:
|
| 54 |
+
ok_order = "Quant" not in ids[:1] # Quant not the preferred top hit
|
| 55 |
+
|
| 56 |
+
print(f"legacy_in_topk={ok_legacy}")
|
| 57 |
+
print(f"quant_not_preferred={ok_order}")
|
| 58 |
+
print(f"pass={ok_legacy and ok_order}")
|
| 59 |
+
return 0 if (ok_legacy and ok_order) else 1
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
if __name__ == "__main__":
|
| 63 |
+
raise SystemExit(main())
|