SNAPKITTYWEST commited on
Commit
f109bf8
Β·
verified Β·
1 Parent(s): 4317457

October 2026 main drop: mirror from GitHub

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .gitattributes +4 -0
  2. .gitignore +6 -0
  3. LICENSE +661 -0
  4. README.md +17 -0
  5. docs/contracts.md +45 -0
  6. docs/images/.gitkeep +0 -0
  7. docs/images/david-demos.png +3 -0
  8. docs/images/david-evidence.png +3 -0
  9. docs/images/david-execution.png +3 -0
  10. docs/images/david-prolog.png +3 -0
  11. evidence/__init__.py +17 -0
  12. evidence/retrieve.py +346 -0
  13. evidence/test_retrieve.py +160 -0
  14. evidence/write_router_smoke.py +133 -0
  15. execution/__init__.py +20 -0
  16. execution/dispatcher.py +99 -0
  17. execution/models.py +66 -0
  18. execution/test_dispatcher.py +97 -0
  19. j/README.md +26 -0
  20. j/eligibility.ijs +93 -0
  21. j/evidence.ijs +218 -0
  22. j/execution.ijs +203 -0
  23. j/fixtures/ann_topk.txt +8 -0
  24. j/run_eligibility_smoke.ijs +33 -0
  25. j/run_evidence_smoke.ijs +5 -0
  26. j/run_execution_smoke.ijs +9 -0
  27. j/run_smoke.ijs +5 -0
  28. j/scorer.ijs +65 -0
  29. j/symbolic_call.ijs +7 -0
  30. logs/agent_ann_smoke.log +10 -0
  31. logs/evidence_hybrid_smoke.log +14 -0
  32. logs/j_evidence_smoke.log +6 -0
  33. logs/j_execution_smoke.log +3 -0
  34. logs/prolog_eligibility_smoke.log +7 -0
  35. logs/prolog_registry_sync.log +3 -0
  36. logs/prolog_selected_cobol.raw +2 -0
  37. logs/prolog_smoke_cobol.log +2 -0
  38. logs/router_smoke.log +4 -0
  39. logs/selected_cobol.txt +6 -0
  40. registry/agent_descriptor.schema.json +69 -0
  41. registry/seed_agents.yaml +132 -0
  42. router/registry_facts.pl +98 -0
  43. router/scorer.py +147 -0
  44. router/symbolic.py +153 -0
  45. router/symbolic_rules.pl +203 -0
  46. router/test_scorer.py +71 -0
  47. router/test_symbolic.py +124 -0
  48. scripts/README_agent_ann.md +11 -0
  49. scripts/README_evidence_hybrid.md +16 -0
  50. 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

  • SHA256: e546b51f7641bd4cc92acb9348a8d3b583600679a1ce0c61b4616bdc2c9405bd
  • Pointer size: 131 Bytes
  • Size of remote file: 109 kB
docs/images/david-evidence.png ADDED

Git LFS Details

  • SHA256: e546b51f7641bd4cc92acb9348a8d3b583600679a1ce0c61b4616bdc2c9405bd
  • Pointer size: 131 Bytes
  • Size of remote file: 109 kB
docs/images/david-execution.png ADDED

Git LFS Details

  • SHA256: e546b51f7641bd4cc92acb9348a8d3b583600679a1ce0c61b4616bdc2c9405bd
  • Pointer size: 131 Bytes
  • Size of remote file: 109 kB
docs/images/david-prolog.png ADDED

Git LFS Details

  • SHA256: e546b51f7641bd4cc92acb9348a8d3b583600679a1ce0c61b4616bdc2c9405bd
  • Pointer size: 131 Bytes
  • Size of remote file: 109 kB
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())