Compare commits
283 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 668d09997a | |||
| 69203dcb12 | |||
| 3b760310a6 | |||
| b39460a507 | |||
| 5177db0611 | |||
| 31f1e6a89d | |||
| f7085d76f6 | |||
| cf0bfd4d76 | |||
| 6b1323d1fe | |||
| fb87fabc2e | |||
| 5adc1c1e64 | |||
| 0d27252999 | |||
| d9f6aad305 | |||
| fda0338134 | |||
| 2b3d165ce2 | |||
| 43f55edb98 | |||
| 4950e1dee2 | |||
| d240364776 | |||
| 6f57f2f30a | |||
| 2c50eca033 | |||
| ac44a4f325 | |||
| 9e05c3ac8b | |||
| 2c83925c88 | |||
| e0cd63923d | |||
| 75204d6134 | |||
| 5ce6e01f27 | |||
| 5cec25a40c | |||
| 855aae6daa | |||
| 43abbcb414 | |||
| 9f33fed823 | |||
| 7a98feb399 | |||
| 626134e5e1 | |||
| e7ed315671 | |||
| d7d9a7dd65 | |||
| 18f6a474e0 | |||
| fb7ebe6a6c | |||
| a0441fb2d1 | |||
| 32547e1e29 | |||
| bcc9ebd2e0 | |||
| c7d4318122 | |||
| 175140ae5a | |||
| 66b98831d3 | |||
| 3e3cc677fb | |||
| 8896d8733b | |||
| 7bbdb55888 | |||
| 79dfd97afa | |||
| 995c309560 | |||
| ea4fec9b1b | |||
| 1de3e1fe38 | |||
| 061f01c081 | |||
| 937b9723d4 | |||
| 367af5feca | |||
| fd73cd0662 | |||
| 81a55bc62c | |||
| 2bc976de10 | |||
| fc568aa0b2 | |||
| b0bfe857fe | |||
| 2ccbee3758 | |||
| 0eee3ec962 | |||
| 130c4cc447 | |||
| f661176b06 | |||
| 3e67743fbf | |||
| 6b8eefd18b | |||
| 682cb84c82 | |||
| dbc345e566 | |||
| afcfae2aea | |||
| eaca50763b | |||
| cb83b8aca6 | |||
| 0b026861a5 | |||
| da8496b45b | |||
| e01b23259a | |||
| 500b0aa1c8 | |||
| fd6e9742a1 | |||
| a6f4d67e15 | |||
| c3ce15a4ab | |||
| 8e8f61f53f | |||
| b7565affe5 | |||
| aba890aa86 | |||
| 0b271267a0 | |||
| a4e97bf102 | |||
| 6ae005ed3a | |||
| 07a55e45f2 | |||
| 2b3875008b | |||
| a51d12d898 | |||
| 9178f3eb91 | |||
| a70fd7bb65 | |||
| 7091b51c87 | |||
| 8627c751a4 | |||
| 6fff3d822c | |||
| a1f12f3185 | |||
| 165cef7aec | |||
| 0221cd4610 | |||
| dfd8da843e | |||
| 29f104fbc0 | |||
| 8669294cf6 | |||
| c9b623b374 | |||
| 15f79ce7b1 | |||
| aa54b8d1c4 | |||
| 60c58260ec | |||
| 8dfe35a1f0 | |||
| 13b60a72ac | |||
| ee40a1bdd2 | |||
| 473fc1f6d7 | |||
| a10a9053b7 | |||
| ec3245c97e | |||
| 8cbb7c0daf | |||
| d40a010538 | |||
| 0cd833513c | |||
| 5e58b6944b | |||
| 0a85ac8cd1 | |||
| 89af2a1281 | |||
| 683689a4f8 | |||
| ddda2f8767 | |||
| 190ec69345 | |||
| b5866dd749 | |||
| c7490b783b | |||
| c7d1b0dca7 | |||
| 9e5ed08635 | |||
| 4bddbbe8f4 | |||
| 1516631a30 | |||
| cf997c2034 | |||
| a0ec67c256 | |||
| 75891b6561 | |||
| 5dc6370a97 | |||
| 4cda3674c4 | |||
| 5ea6f06a6d | |||
| 116bd0e39b | |||
| 208ff42915 | |||
| 972a103a97 | |||
| 2f735aedf9 | |||
| f4c721d934 | |||
| b7a951685b | |||
| abb2461244 | |||
| b268c55783 | |||
| f0182fb2c9 | |||
| 8d05b594e2 | |||
| edaaecc982 | |||
| 2e81c9e61b | |||
| 2b723760d1 | |||
| c5aed7a451 | |||
| d91afc61a4 | |||
| 6eeb0ee9c0 | |||
| b9c232ee63 | |||
| 7291d23587 | |||
| da1d196dc5 | |||
| b0de49a87a | |||
| 27ff580dae | |||
| ece764f19e | |||
| f91a02548e | |||
| 8fbd058b90 | |||
| c8f9a4def9 | |||
| d8b6c0b471 | |||
| 52cd94fcd1 | |||
| f1f72dfe8f | |||
| d4676bc870 | |||
| 64769883ae | |||
| 635674c066 | |||
| 50bc35516a | |||
| 18309f50db | |||
| 60784e7875 | |||
| f3148bfe52 | |||
| f315620bf7 | |||
| 97357d62b9 | |||
| 0c3651fceb | |||
| 5a5d47d36e | |||
| 143c6e9b3a | |||
| 48e4784fcd | |||
| 78af5069c1 | |||
| 18963266d7 | |||
| d039a019cb | |||
| d1c0281053 | |||
| 870e4ca4e6 | |||
| fd82524977 | |||
| 3f76c75919 | |||
| fe764f72d8 | |||
| 26ab566f38 | |||
| 18ca21fa45 | |||
| e32fdc22f4 | |||
| b02b802427 | |||
| 301192b974 | |||
| f2aeb5eb30 | |||
| a27ee8f7da | |||
| 4d1895c223 | |||
| d7fe70520a | |||
| 7325a34c07 | |||
| 03c44923c8 | |||
| dc0adf5751 | |||
| eae21797f7 | |||
| ea6c7d5f0f | |||
| 389ef26db3 | |||
| 8a5c0de162 | |||
| 80aecb646d | |||
| b7b4000aab | |||
| 11ecb923ba | |||
| f46d5a4591 | |||
| 743e78d514 | |||
| 3743221aa5 | |||
| a8863d49d0 | |||
| c5f733af45 | |||
| c09d2c6e20 | |||
| 6c5dec761c | |||
| 74b91fae99 | |||
| b7ff37118c | |||
| ef012ea1e5 | |||
| 04d5d695e0 | |||
| 78bff91395 | |||
| c63ca5352f | |||
| 3f157150e4 | |||
| f1e1a45a05 | |||
| 996295d5c1 | |||
| 58a5bbe790 | |||
| dd89be3b03 | |||
| ed7c27324d | |||
| 05fe13d80b | |||
| 0eb009ad5a | |||
| 3fde1f6cbb | |||
| 481e2d494c | |||
| eb830bb613 | |||
| dd6d4591ae | |||
| de6443f3ae | |||
| 5a63b649f9 | |||
| c7e22c0dbe | |||
| e69a847cf2 | |||
| 21815ad7c0 | |||
| a051edd82d | |||
| ee0ad99e17 | |||
| bc5a2da0f2 | |||
| 089e824a92 | |||
| 93e8874813 | |||
| cc0b6183bf | |||
| bf472dad8a | |||
| a6e0ffea8f | |||
| e459c7d54b | |||
| 71f6683fda | |||
| 7827ca043b | |||
| b595dca8c7 | |||
| d5f34642d7 | |||
| b1866946d1 | |||
| 05082c6fdf | |||
| 51b072965c | |||
| d5940856be | |||
| 0d163f8165 | |||
| 1889915a81 | |||
| 3ca01ae0b0 | |||
| 6e283d16c3 | |||
| 41b2021d8c | |||
| 3add7024db | |||
| a148a2b1f4 | |||
| 7310ef34a4 | |||
| 4b8169da28 | |||
| 62a4a20529 | |||
| 8c68b28421 | |||
| 66c55b7397 | |||
| 48343300e2 | |||
| f17a38f60b | |||
| aba5e960aa | |||
| d818e0d162 | |||
| ba9c972faa | |||
| 17b6c7bca5 | |||
| 2969a83a71 | |||
| 7f58261498 | |||
| defb240b6e | |||
| b158a8ec2d | |||
| 2b847ac2ae | |||
| 5f7b19403f | |||
| 80131fa160 | |||
| 521291d80f | |||
| 0f43460f84 | |||
| 08409dd472 | |||
| 34f5980973 | |||
| 7b012033a6 | |||
| 37f8dba6c3 | |||
| c7bdaa076b | |||
| 12d6d80024 | |||
| d8147cac49 | |||
| a16c2a38d6 | |||
| 843d07b636 | |||
| dc6fd2752d | |||
| 15e4067954 | |||
| 6e34fb9de5 | |||
| 5843ceef82 | |||
| 1cd0c611f2 | |||
| 6ed582f4b5 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -22,6 +22,9 @@
|
|||||||
go.work
|
go.work
|
||||||
go.work.sum
|
go.work.sum
|
||||||
|
|
||||||
|
# Local ReleaseKit delivery credentials
|
||||||
|
.releasekit/
|
||||||
|
|
||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
@@ -67,4 +70,3 @@ Icon
|
|||||||
.AppleDesktop
|
.AppleDesktop
|
||||||
Network Trash Folder
|
Network Trash Folder
|
||||||
Temporary Items
|
Temporary Items
|
||||||
.apdisk
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
Please review `docs/development.md` for initial orientation in this repository
|
Start with `docs/development.md` for orientation and use its policy reading guide
|
||||||
and follow its task-specific reading guide.
|
to select the policies applicable to the task. Do not read the entire suite by
|
||||||
|
default.
|
||||||
|
|
||||||
Review and follow `docs/policy/agents.md` before recommending, planning, or
|
Read and follow `docs/policy/agents.md` before recommending, planning, or
|
||||||
implementing any changes to this repository.
|
implementing repository changes.
|
||||||
|
|||||||
674
COPYING
674
COPYING
@@ -1,674 +0,0 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
23
COPYRIGHT
Normal file
23
COPYRIGHT
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
PromptKit
|
||||||
|
|
||||||
|
Copyright (C) 2026 Eric Rakestraw
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
This library is free software: you can redistribute it and/or modify it
|
||||||
|
under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
The GNU Lesser General Public License version 3 is provided in LICENSE.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
Policy documents copied or adapted from Standard Repository Policies remain licensed
|
||||||
|
under CC-BY-SA-4.0. See docs/oss/attribution.md for the affected material,
|
||||||
|
upstream credit, and license links.
|
||||||
|
|
||||||
|
Third-party code and assets are subject to their respective license terms.
|
||||||
693
LICENSE
693
LICENSE
@@ -1,21 +1,3 @@
|
|||||||
Promptkit
|
|
||||||
Copyright (C) 2026 Eric Rakestraw
|
|
||||||
|
|
||||||
SPDX-License-Identifier: LGPL-3.0-or-later
|
|
||||||
|
|
||||||
This library is free software: you can redistribute it and/or modify it
|
|
||||||
under the terms of the GNU Lesser General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This library is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Lesser General Public License for more details.
|
|
||||||
|
|
||||||
The LGPL version 3 text follows. It incorporates the GPL version 3 text
|
|
||||||
provided in COPYING. Dependency code and assets retain their own licenses.
|
|
||||||
|
|
||||||
GNU LESSER GENERAL PUBLIC LICENSE
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
@@ -181,3 +163,678 @@ whether future versions of the GNU Lesser General Public License shall
|
|||||||
apply, that proxy's public statement of acceptance of any version is
|
apply, that proxy's public statement of acceptance of any version is
|
||||||
permanent authorization for you to choose that version for the
|
permanent authorization for you to choose that version for the
|
||||||
Library.
|
Library.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
|
|||||||
68
README.md
68
README.md
@@ -4,11 +4,12 @@ Promptkit is a reusable Go library for preparing and executing prompt-defined
|
|||||||
LLM workflows. Its module path is:
|
LLM workflows. Its module path is:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
gitea.maximumdirect.net/eric/promptkit
|
go.maximumdirect.net/promptkit
|
||||||
```
|
```
|
||||||
|
|
||||||
The root `promptkit` package provides the supported public engine. Consumers
|
The root `promptkit` package provides the supported public engine. Consumers
|
||||||
can configure filesystem or in-memory prompt, profile, and schema sources,
|
can configure file, embedded, or programmatic backend, profile, and prompt
|
||||||
|
definitions alongside filesystem or in-memory schemas,
|
||||||
prepare requests without generation, run requests with the built-in
|
prepare requests without generation, run requests with the built-in
|
||||||
OpenAI-compatible client, or inject their own model client and artifact reader.
|
OpenAI-compatible client, or inject their own model client and artifact reader.
|
||||||
|
|
||||||
@@ -20,12 +21,23 @@ Run the maintained offline preparation example from the repository root:
|
|||||||
go run ./examples/go-library/prepare
|
go run ./examples/go-library/prepare
|
||||||
```
|
```
|
||||||
|
|
||||||
It loads a repository-local prompt, supplies an in-memory profile and inline
|
It composes two explicitly selected synthetic collections, imports
|
||||||
input, and prints deterministic preparation metadata without contacting a
|
source-relative prompt content, supplies an inline input, and prepares through
|
||||||
provider or requiring credentials. Read the
|
a scoped view. The example also demonstrates collection inventories and
|
||||||
[example source](examples/go-library/prepare/main.go), the
|
namespace-filtered discovery, without contacting a provider or requiring
|
||||||
|
credentials.
|
||||||
|
The example disables the bundled OpenRouter configuration to keep its
|
||||||
|
inventories limited to its own offline fixtures.
|
||||||
|
The [consumer guide](docs/consumers/pkg-promptkit.md) also shows a local backend,
|
||||||
|
ordered file/embedded sources, programmatic mappings, and how to replace an
|
||||||
|
engine when source files change. Applications adapting the prior keyed
|
||||||
|
configuration or versioned-prompt format can consult the
|
||||||
|
[configuration migration guide](docs/consumers/configuration-migration.md).
|
||||||
|
See the [example source](examples/go-library/prepare/main.go), the
|
||||||
[Go package consumer guide](docs/consumers/pkg-promptkit.md), and the
|
[Go package consumer guide](docs/consumers/pkg-promptkit.md), and the
|
||||||
[framework format reference](docs/formats.md) to build a consumer workflow.
|
[framework format reference](docs/formats.md) to build a consumer workflow.
|
||||||
|
The consumer guide also covers explicit, live smoke diagnostics and interpreting
|
||||||
|
their factual reports; they are separate from the offline quickstart.
|
||||||
|
|
||||||
Contributors should start with the [development guide](docs/development.md).
|
Contributors should start with the [development guide](docs/development.md).
|
||||||
The [architecture policy](docs/policy/architecture.md) defines the library
|
The [architecture policy](docs/policy/architecture.md) defines the library
|
||||||
@@ -33,6 +45,36 @@ boundary and constraints that framework work must preserve.
|
|||||||
|
|
||||||
## Release Guidance
|
## Release Guidance
|
||||||
|
|
||||||
|
The [v0.18.0 release notes](docs/releases/v0.18.0.md) cover the default-on
|
||||||
|
OpenRouter backend and profiles, recorded configuration normalization, and
|
||||||
|
ConfigKit v1.1. The notes explain inventory changes and the bundle opt-out;
|
||||||
|
the Go 1.27 minimum remains unchanged.
|
||||||
|
|
||||||
|
The [v0.17.0 release notes](docs/releases/v0.17.0.md) cover the vanity module
|
||||||
|
path, ConfigKit v1, identified resource lists, removal of prompt-version
|
||||||
|
metadata, and the Go 1.27 minimum. This is a breaking pre-v1 migration.
|
||||||
|
|
||||||
|
Consumers using the former Go module path should follow the
|
||||||
|
[module-path migration guidance](docs/consumers/configuration-migration.md#update-go-imports).
|
||||||
|
|
||||||
|
The [v0.16.0 release notes](docs/releases/v0.16.0.md) cover explicit smoke
|
||||||
|
diagnostics, configuration collections, scoped engine views, and discovery.
|
||||||
|
Upgrading consumers should also review the current identified-resource and
|
||||||
|
ConfigKit v1 changes in the [migration guide](docs/consumers/configuration-migration.md).
|
||||||
|
|
||||||
|
The [v0.15.0 release notes](docs/releases/v0.15.0.md) cover error-only
|
||||||
|
validation of concrete requests with `Engine.ValidateRequest`.
|
||||||
|
|
||||||
|
Consumers upgrading to `v0.12.0` should read the
|
||||||
|
[v0.12.0 release notes](docs/releases/v0.12.0.md) for the intentional ConfigKit
|
||||||
|
configuration and public construction API migration.
|
||||||
|
|
||||||
|
The [v0.11.2 release notes](docs/releases/v0.11.2.md) cover policy alignment
|
||||||
|
and copied-policy attribution updates.
|
||||||
|
|
||||||
|
The [v0.11.1 release notes](docs/releases/v0.11.1.md) cover the MIT-licensed
|
||||||
|
catalog updates and licensing documentation improvements.
|
||||||
|
|
||||||
Consumers upgrading to `v0.11.0` should read the
|
Consumers upgrading to `v0.11.0` should read the
|
||||||
[v0.11.0 changelog and migration guide](docs/releases/v0.11.0.md), particularly
|
[v0.11.0 changelog and migration guide](docs/releases/v0.11.0.md), particularly
|
||||||
the default retry behavior and license change.
|
the default retry behavior and license change.
|
||||||
@@ -71,16 +113,6 @@ HTTP application built on Promptkit.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Promptkit is licensed under the [GNU Lesser General Public License version 3
|
Promptkit is licensed under the [LGPL v3.0 or later](COPYRIGHT).
|
||||||
or later](LICENSE) (`LGPL-3.0-or-later`). The accompanying [GPLv3 text](COPYING)
|
|
||||||
is incorporated by LGPLv3. This maintainer-authorized change applies from this
|
|
||||||
revision forward; existing release tags and their license notices are unchanged.
|
|
||||||
Dependencies, including the backend catalogs, retain their own licenses.
|
|
||||||
|
|
||||||
Applications may use their own licenses subject to the LGPL's combined-work
|
[Open Source attribution notices](docs/oss/attribution.md) are available.
|
||||||
conditions. Distributors of statically linked Go binaries must account for the
|
|
||||||
applicable notices, license copies, library source, and materials and permissions
|
|
||||||
needed to recombine or relink with a modified library; installation information
|
|
||||||
may also be required. Providing library source alone is not necessarily enough.
|
|
||||||
See section 4 of the [LGPL text](LICENSE) for the governing requirements, rather
|
|
||||||
than treating this summary as a complete distribution checklist.
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit"
|
"go.maximumdirect.net/promptkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAppendedMessageRoleConstantsAreStrings(t *testing.T) {
|
func TestAppendedMessageRoleConstantsAreStrings(t *testing.T) {
|
||||||
@@ -29,15 +29,7 @@ func TestAppendedMessagesComposeFrozenEffectivePrompt(t *testing.T) {
|
|||||||
{Role: promptkit.RoleUser, Content: "corrective request", CacheControl: cacheControl},
|
{Role: promptkit.RoleUser, Content: "corrective request", CacheControl: cacheControl},
|
||||||
}
|
}
|
||||||
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "output"}}
|
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "output"}}
|
||||||
engine, err := promptkit.NewEngine(
|
engine := newSimpleEngine(t, "configured prompt", promptkit.WithLLMClient(client))
|
||||||
promptkit.Config{},
|
|
||||||
promptkit.WithPromptFS(contractPromptFS("prompt", "profile", "configured prompt"), "."),
|
|
||||||
promptkit.WithProfiles(promptkit.Profile{ID: "profile", Endpoint: "http://example.test/v1", Model: "model"}),
|
|
||||||
promptkit.WithLLMClient(client),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("construct engine: %v", err)
|
|
||||||
}
|
|
||||||
request := promptkit.RunRequest{PromptID: "prompt", AppendedMessages: appended}
|
request := promptkit.RunRequest{PromptID: "prompt", AppendedMessages: appended}
|
||||||
|
|
||||||
plain, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "prompt"})
|
plain, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "prompt"})
|
||||||
@@ -101,14 +93,9 @@ func TestAppendedMessagesComposeFrozenEffectivePrompt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInvalidAppendedMessagesFailBeforeSourceOrModelWork(t *testing.T) {
|
func TestInvalidAppendedMessagesFailBeforeModelWork(t *testing.T) {
|
||||||
promptSource := &inspectionCountingFS{}
|
|
||||||
var modelCalls atomic.Int64
|
var modelCalls atomic.Int64
|
||||||
engine, err := promptkit.NewEngine(
|
engine, err := promptkit.NewEngine(promptkit.Config{}, promptkit.WithLLMClient(countingLLMClient{calls: &modelCalls}))
|
||||||
promptkit.Config{},
|
|
||||||
promptkit.WithPromptFS(promptSource, "."),
|
|
||||||
promptkit.WithLLMClient(countingLLMClient{calls: &modelCalls}),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("construct engine: %v", err)
|
t.Fatalf("construct engine: %v", err)
|
||||||
}
|
}
|
||||||
@@ -136,9 +123,6 @@ func TestInvalidAppendedMessagesFailBeforeSourceOrModelWork(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if promptSource.opens.Load() != 0 {
|
|
||||||
t.Fatalf("invalid appended message opened prompt sources %d times", promptSource.opens.Load())
|
|
||||||
}
|
|
||||||
if modelCalls.Load() != 0 {
|
if modelCalls.Load() != 0 {
|
||||||
t.Fatalf("invalid appended message invoked the model %d times", modelCalls.Load())
|
t.Fatalf("invalid appended message invoked the model %d times", modelCalls.Load())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
artifactadapter "gitea.maximumdirect.net/eric/promptkit/internal/artifact"
|
artifactadapter "go.maximumdirect.net/promptkit/internal/artifact"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
"go.maximumdirect.net/promptkit/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errNilArtifactReaderResponse = errors.New("artifact reader returned nil artifact without error")
|
var errNilArtifactReaderResponse = errors.New("artifact reader returned nil artifact without error")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
"go.maximumdirect.net/promptkit/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPublicArtifactReaderAdapterCopiesBody(t *testing.T) {
|
func TestPublicArtifactReaderAdapterCopiesBody(t *testing.T) {
|
||||||
|
|||||||
237
assembled_workflow_benchmark_test.go
Normal file
237
assembled_workflow_benchmark_test.go
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
assembledPreparedSink *promptkit.PreparedRun
|
||||||
|
assembledResultSink *promptkit.RunResult
|
||||||
|
assembledDetailsSink promptkit.PreparedRun
|
||||||
|
assembledHandleSink *promptkit.PreparedExecution
|
||||||
|
)
|
||||||
|
|
||||||
|
type assembledWorkflowClient struct{ content string }
|
||||||
|
|
||||||
|
func (client assembledWorkflowClient) Generate(
|
||||||
|
context.Context,
|
||||||
|
promptkit.GenerateRequest,
|
||||||
|
) (*promptkit.GenerateResponse, error) {
|
||||||
|
return &promptkit.GenerateResponse{Content: client.content}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type assembledWorkflowFixture struct {
|
||||||
|
engine *promptkit.Engine
|
||||||
|
request promptkit.RunRequest
|
||||||
|
expectedMessage string
|
||||||
|
response string
|
||||||
|
}
|
||||||
|
|
||||||
|
func assembledWorkflowSchema(b *testing.B) []byte {
|
||||||
|
b.Helper()
|
||||||
|
properties := make(map[string]any, 256)
|
||||||
|
for index := range 256 {
|
||||||
|
properties[fmt.Sprintf("property-%03d", index)] = map[string]any{"type": "string"}
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(map[string]any{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": properties,
|
||||||
|
"additionalProperties": false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func assembledWorkflowExtras(entries int) map[string]any {
|
||||||
|
providers := make(map[string]any, entries)
|
||||||
|
for index := range entries {
|
||||||
|
providers[fmt.Sprintf("provider-%03d", index)] = map[string]any{
|
||||||
|
"enabled": true,
|
||||||
|
"labels": []any{fmt.Sprintf("tier-%d", index%4), fmt.Sprintf("region-%d", index%3)},
|
||||||
|
"limits": map[string]any{"burst": index + 1, "period": "minute"},
|
||||||
|
"routes": []any{map[string]any{"weight": index + 1, "fallback": true}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map[string]any{"provider_options": providers}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAssembledWorkflowFixture(b *testing.B, name string) assembledWorkflowFixture {
|
||||||
|
b.Helper()
|
||||||
|
message := "Hello {{.name}}"
|
||||||
|
response := "ok"
|
||||||
|
output := map[string]any{"format": "text", "validation_mode": "none"}
|
||||||
|
profile := map[string]any{"id": "profile", "endpoint": "http://example.test/v1", "model": "benchmark-model"}
|
||||||
|
options := []promptkit.Option{promptkit.WithLLMClient(assembledWorkflowClient{content: response})}
|
||||||
|
var expectedMessage string
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "small":
|
||||||
|
expectedMessage = "Hello Ada"
|
||||||
|
case "large_message":
|
||||||
|
prefix := strings.Repeat("x", 256*1024)
|
||||||
|
message = prefix + "{{.name}}"
|
||||||
|
expectedMessage = prefix + "Ada"
|
||||||
|
case "mutable_payload":
|
||||||
|
message = `{"subject":"{{.name}}"}`
|
||||||
|
expectedMessage = `{"subject":"Ada"}`
|
||||||
|
response = "{}"
|
||||||
|
profile["extra_params"] = assembledWorkflowExtras(64)
|
||||||
|
output = map[string]any{
|
||||||
|
"format": "json", "validation_mode": "json_schema", "schema_path": "workflow-schema.json",
|
||||||
|
}
|
||||||
|
options[0] = promptkit.WithLLMClient(assembledWorkflowClient{content: response})
|
||||||
|
options = append(options, promptkit.WithSchemaFS(fstest.MapFS{
|
||||||
|
"workflow-schema.json": &fstest.MapFile{Data: assembledWorkflowSchema(b)},
|
||||||
|
}, "."))
|
||||||
|
default:
|
||||||
|
b.Fatalf("unknown assembled workflow fixture %q", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := map[string]any{
|
||||||
|
"profiles": []any{profile},
|
||||||
|
"prompts": []any{map[string]any{"id": "workflow",
|
||||||
|
"default_profile": "profile",
|
||||||
|
"messages": []any{map[string]any{"role": "user", "content": message}},
|
||||||
|
"output": output,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
||||||
|
Source: configkit.Mapping("assembled workflow benchmark", config),
|
||||||
|
}}}, options...)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
request := promptkit.RunRequest{
|
||||||
|
PromptID: "workflow", Vars: map[string]string{"name": "Ada"},
|
||||||
|
}
|
||||||
|
return assembledWorkflowFixture{
|
||||||
|
engine: engine, request: request, expectedMessage: expectedMessage, response: response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyAssembledWorkflowFixture(b *testing.B, fixture assembledWorkflowFixture) {
|
||||||
|
b.Helper()
|
||||||
|
ctx := context.Background()
|
||||||
|
prepared, err := fixture.engine.Prepare(ctx, fixture.request)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
if prepared.PromptID != "workflow" || len(prepared.Messages) != 1 ||
|
||||||
|
prepared.Messages[0].Content != fixture.expectedMessage {
|
||||||
|
b.Fatalf("prepared fixture output is invalid: %+v", prepared)
|
||||||
|
}
|
||||||
|
handle, err := fixture.engine.PrepareExecution(ctx, fixture.request)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
details := handle.Details()
|
||||||
|
if details.PromptID != "workflow" || len(details.Messages) != 1 || details.Messages[0].Content != fixture.expectedMessage {
|
||||||
|
handle.Discard()
|
||||||
|
b.Fatalf("prepared execution details are invalid: %+v", details)
|
||||||
|
}
|
||||||
|
handle.Discard()
|
||||||
|
result, err := fixture.engine.Run(ctx, fixture.request)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.RawOutput != fixture.response {
|
||||||
|
b.Fatalf("run output=%q, want %q", result.RawOutput, fixture.response)
|
||||||
|
}
|
||||||
|
if fixture.response == "{}" && result.Validation.Status != promptkit.ValidationPassed {
|
||||||
|
b.Fatalf("structured response validation=%+v, want passed", result.Validation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEnginePreparation(b *testing.B) {
|
||||||
|
for _, name := range []string{"small", "large_message", "mutable_payload"} {
|
||||||
|
b.Run(name, func(b *testing.B) {
|
||||||
|
fixture := newAssembledWorkflowFixture(b, name)
|
||||||
|
verifyAssembledWorkflowFixture(b, fixture)
|
||||||
|
ctx := context.Background()
|
||||||
|
b.Run("prepare", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for b.Loop() {
|
||||||
|
prepared, err := fixture.engine.Prepare(ctx, fixture.request)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
assembledPreparedSink = prepared
|
||||||
|
}
|
||||||
|
b.StopTimer()
|
||||||
|
assembledPreparedSink = nil
|
||||||
|
})
|
||||||
|
b.Run("prepared_execution_creation_and_discard", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for b.Loop() {
|
||||||
|
handle, err := fixture.engine.PrepareExecution(ctx, fixture.request)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
handle.Discard()
|
||||||
|
assembledHandleSink = handle
|
||||||
|
}
|
||||||
|
b.StopTimer()
|
||||||
|
assembledHandleSink = nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEngineRun(b *testing.B) {
|
||||||
|
for _, name := range []string{"small", "large_message", "mutable_payload"} {
|
||||||
|
b.Run(name, func(b *testing.B) {
|
||||||
|
fixture := newAssembledWorkflowFixture(b, name)
|
||||||
|
verifyAssembledWorkflowFixture(b, fixture)
|
||||||
|
ctx := context.Background()
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for b.Loop() {
|
||||||
|
result, err := fixture.engine.Run(ctx, fixture.request)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
assembledResultSink = result
|
||||||
|
}
|
||||||
|
b.StopTimer()
|
||||||
|
assembledResultSink = nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPreparedExecutionDetails(b *testing.B) {
|
||||||
|
for _, name := range []string{"small", "large_message", "mutable_payload"} {
|
||||||
|
b.Run(name, func(b *testing.B) {
|
||||||
|
fixture := newAssembledWorkflowFixture(b, name)
|
||||||
|
verifyAssembledWorkflowFixture(b, fixture)
|
||||||
|
handle, err := fixture.engine.PrepareExecution(context.Background(), fixture.request)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
defer handle.Discard()
|
||||||
|
initial := handle.Details()
|
||||||
|
if len(initial.Messages) != 1 || initial.Messages[0].Content != fixture.expectedMessage {
|
||||||
|
b.Fatalf("prepared details fixture is invalid: %+v", initial)
|
||||||
|
}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for b.Loop() {
|
||||||
|
assembledDetailsSink = handle.Details()
|
||||||
|
}
|
||||||
|
b.StopTimer()
|
||||||
|
assembledDetailsSink = promptkit.PreparedRun{}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
205
backends.go
205
backends.go
@@ -1,107 +1,134 @@
|
|||||||
package promptkit
|
package promptkit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/backend"
|
"go.maximumdirect.net/configkit"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
"go.maximumdirect.net/promptkit/internal/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BackendOpenRouter is the reserved ID of Promptkit's built-in OpenRouter
|
// BackendOpenRouter is the conventional ID for an OpenRouter backend.
|
||||||
// backend.
|
|
||||||
const BackendOpenRouter = backend.OpenRouterID
|
const BackendOpenRouter = backend.OpenRouterID
|
||||||
|
|
||||||
// BackendRakestrawHome is the reserved ID of Promptkit's built-in
|
// BackendRakestrawHome is the conventional ID for a Rakestrawhome backend.
|
||||||
// Rakestrawhome backend.
|
|
||||||
const BackendRakestrawHome = backend.RakestrawHomeID
|
const BackendRakestrawHome = backend.RakestrawHomeID
|
||||||
|
|
||||||
// BackendLocal is the case-sensitive conventional ID used by [LocalBackend].
|
// BackendLocal is a conventional ID for a caller-configured local backend.
|
||||||
// It is not a built-in or reserved backend and must be registered with
|
|
||||||
// [WithBackend].
|
|
||||||
const BackendLocal = "local"
|
const BackendLocal = "local"
|
||||||
|
|
||||||
// Backend configures one engine-scoped OpenAI-compatible backend.
|
// BackendConfig is a presence-aware convenience value for an identified backend
|
||||||
//
|
// definition. Nil pointer fields are omitted, while non-nil pointers preserve
|
||||||
// Backend has no stable JSON representation. Use keyed literals so additions
|
// explicit zero or blank values. A nil ExtraParams map is omitted; a non-nil
|
||||||
// to this configuration value do not break source compatibility.
|
// empty map supplies an empty mapping. A nil SmokeTests pointer is omitted; a
|
||||||
type Backend struct {
|
// non-nil pointer supplies a smoke_tests mapping. Use raw ConfigKit Mapping
|
||||||
// ID is the stable, case-sensitive registry key. NewEngine trims it and
|
// inputs for explicit null. BackendConfig has no stable JSON representation.
|
||||||
// requires a non-blank value. Built-in backend IDs are reserved.
|
type BackendConfig struct {
|
||||||
|
// ID is the local backend identity within its resource list.
|
||||||
ID string
|
ID string
|
||||||
// Endpoint is the OpenAI-compatible base endpoint. NewEngine trims it and
|
// Endpoint is the required absolute HTTP(S) base URL after all layers merge.
|
||||||
// requires an absolute HTTP or HTTPS URL with a host and without user
|
Endpoint *string
|
||||||
// information, a query string, or a fragment. Paths are allowed.
|
// APIKeyEnv optionally names an environment variable, not a credential.
|
||||||
Endpoint string
|
APIKeyEnv *string
|
||||||
// APIKeyEnv optionally names an environment lookup source for an API key.
|
// ConcurrencyLimit is zero for unlimited generation; negative is invalid.
|
||||||
// NewEngine trims it and requires the portable form [A-Za-z_][A-Za-z0-9_]*.
|
ConcurrencyLimit *int
|
||||||
// A direct RunRequest.APIKey takes precedence. When no usable credential is
|
// QueueCapacity defaults to 1024 for positive concurrency when omitted.
|
||||||
// available, the built-in client omits Authorization; injected clients own
|
// An explicit value, including zero, requires positive concurrency.
|
||||||
// their own credential-resolution behavior. Store only the name, never a
|
|
||||||
// credential value.
|
|
||||||
APIKeyEnv string
|
|
||||||
// ExtraParams contains backend-wide request defaults. Values must be
|
|
||||||
// JSON-compatible, finite, acyclic, and keyed by non-empty strings. Keys
|
|
||||||
// must not be model, session_id, messages, temperature, max_tokens, top_p,
|
|
||||||
// service_tier, reasoning_effort, or response_format. An empty map supplies
|
|
||||||
// no defaults. NewEngine deeply copies the map and rejects excessively deep
|
|
||||||
// or large values for safety.
|
|
||||||
ExtraParams map[string]any
|
|
||||||
// ConcurrencyLimit is the maximum number of simultaneous model-generation
|
|
||||||
// calls allowed for this backend within one Engine. Zero leaves the backend
|
|
||||||
// unlimited. A negative value makes NewEngine fail with ErrInvalidConfig.
|
|
||||||
ConcurrencyLimit int
|
|
||||||
// QueueCapacity controls how many additional Run or RunPrepared calls may
|
|
||||||
// be admitted beyond ConcurrencyLimit. Nil uses 1024 when ConcurrencyLimit
|
|
||||||
// is positive; a pointer uses its exact value, including zero. The pointed-to
|
|
||||||
// value must be non-negative, and QueueCapacity must be nil when
|
|
||||||
// ConcurrencyLimit is zero. Their sum must fit in an int. WithBackend copies
|
|
||||||
// the value and does not retain the pointer.
|
|
||||||
QueueCapacity *int
|
QueueCapacity *int
|
||||||
|
// ExtraParams supplies JSON-compatible provider request defaults.
|
||||||
|
ExtraParams map[string]any
|
||||||
|
// SmokeTests supplies optional diagnostic defaults and HTTP probe settings.
|
||||||
|
SmokeTests *BackendSmokeTestConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalBackend returns a caller-owned Backend for a conventional local
|
// BackendSmokeTestConfig contains optional diagnostic settings associated with
|
||||||
// OpenAI-compatible endpoint. It sets ID to BackendLocal and copies endpoint
|
// a backend. Nil nested fields are omitted by BackendSource.
|
||||||
// and concurrencyLimit into Endpoint and ConcurrencyLimit without
|
type BackendSmokeTestConfig struct {
|
||||||
// normalization or validation. APIKeyEnv, ExtraParams, and QueueCapacity keep
|
// DefaultProfile is the exact profile ID selected as the backend's
|
||||||
// their zero values.
|
// diagnostic inference default. A supplied value must not be blank; the ID
|
||||||
//
|
// is resolved only when that default is selected.
|
||||||
// LocalBackend does not read environment variables, register the value, or
|
DefaultProfile *string
|
||||||
// mutate engine or package state. Supply the returned value through
|
// Reachability optionally selects a full HTTP destination for reachability
|
||||||
// [WithBackend]; [NewEngine] then applies the ordinary backend validation and
|
// diagnostics. A nil endpoint uses the effective backend endpoint.
|
||||||
// concurrency semantics, including default queue capacity for a positive
|
Reachability *ReachabilityProbeConfig
|
||||||
// limit, unlimited behavior for zero, and ErrInvalidConfig for a negative
|
// Authentication optionally declares a named authentication diagnostic.
|
||||||
// limit.
|
// A supplied object requires Kind.
|
||||||
func LocalBackend(endpoint string, concurrencyLimit int) Backend {
|
Authentication *AuthenticationProbeConfig
|
||||||
return Backend{
|
|
||||||
ID: BackendLocal,
|
|
||||||
Endpoint: endpoint,
|
|
||||||
ConcurrencyLimit: concurrencyLimit,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithBackend adds one Backend registration to the constructed Engine.
|
// ReachabilityProbeConfig configures an optional reachability destination.
|
||||||
//
|
type ReachabilityProbeConfig struct {
|
||||||
// Registrations accumulate in option order. Every normalized ID must be unique
|
// Endpoint is an absolute HTTP(S) URL, including any desired path or query.
|
||||||
// across consumer registrations and built-ins; a duplicate or invalid
|
// Nil selects the effective backend endpoint; an explicit blank value is
|
||||||
// definition makes NewEngine fail with ErrInvalidConfig. Built-in IDs,
|
// invalid.
|
||||||
// including [BackendOpenRouter] and [BackendRakestrawHome], cannot be
|
Endpoint *string
|
||||||
// replaced. The immutable registration is scoped to the resulting Engine and
|
}
|
||||||
// cannot be enumerated, replaced, removed, or mutated after construction.
|
|
||||||
// WithBackend does not install package-global state.
|
// AuthenticationProbeConfig configures an optional named authentication
|
||||||
func WithBackend(backend Backend) Option {
|
// diagnostic.
|
||||||
queueCapacity := 0
|
type AuthenticationProbeConfig struct {
|
||||||
queueCapacitySet := backend.QueueCapacity != nil
|
// Kind selects the authentication adapter. Supported values are "none" and
|
||||||
if queueCapacitySet {
|
// "openrouter_key"; surrounding whitespace and case are normalized.
|
||||||
queueCapacity = *backend.QueueCapacity
|
Kind *string
|
||||||
}
|
// Endpoint is an absolute HTTP(S) URL, including any desired path or query.
|
||||||
return optionFunc(func(options *engineOptions) error {
|
// Nil selects the adapter's backend-relative destination; an explicit blank
|
||||||
options.internal.AddBackend(domain.Backend{
|
// value is invalid.
|
||||||
ID: backend.ID,
|
Endpoint *string
|
||||||
Endpoint: backend.Endpoint,
|
}
|
||||||
APIKeyEnv: backend.APIKeyEnv,
|
|
||||||
ExtraParams: backend.ExtraParams,
|
// BackendSource creates a labeled ConfigKit mapping containing backend records
|
||||||
ConcurrencyLimit: backend.ConcurrencyLimit,
|
// in caller order. IDs are explicit and duplicates are left for loading to reject.
|
||||||
QueueCapacity: queueCapacity,
|
// It encodes fields without validating them; NewEngine validates the final
|
||||||
QueueCapacitySet: queueCapacitySet,
|
// effective configuration after all selected source layers are combined.
|
||||||
})
|
// Mapping captures a copy of supported input values at creation; unsupported
|
||||||
return nil
|
// values fail when the source is loaded.
|
||||||
})
|
func BackendSource(label string, backends []BackendConfig) configkit.Source {
|
||||||
|
definitions := make([]any, 0, len(backends))
|
||||||
|
for _, config := range backends {
|
||||||
|
fields := map[string]any{"id": config.ID}
|
||||||
|
if config.Endpoint != nil {
|
||||||
|
fields["endpoint"] = *config.Endpoint
|
||||||
|
}
|
||||||
|
if config.APIKeyEnv != nil {
|
||||||
|
fields["api_key_env"] = *config.APIKeyEnv
|
||||||
|
}
|
||||||
|
if config.ConcurrencyLimit != nil {
|
||||||
|
fields["concurrency_limit"] = *config.ConcurrencyLimit
|
||||||
|
}
|
||||||
|
if config.QueueCapacity != nil {
|
||||||
|
fields["queue_capacity"] = *config.QueueCapacity
|
||||||
|
}
|
||||||
|
if config.ExtraParams != nil {
|
||||||
|
fields["extra_params"] = providerMapForMapping(config.ExtraParams)
|
||||||
|
}
|
||||||
|
if config.SmokeTests != nil {
|
||||||
|
smokeTests := make(map[string]any)
|
||||||
|
if config.SmokeTests.DefaultProfile != nil {
|
||||||
|
smokeTests["default_profile"] = *config.SmokeTests.DefaultProfile
|
||||||
|
}
|
||||||
|
if config.SmokeTests.Reachability != nil {
|
||||||
|
reachability := make(map[string]any)
|
||||||
|
if config.SmokeTests.Reachability.Endpoint != nil {
|
||||||
|
reachability["endpoint"] = *config.SmokeTests.Reachability.Endpoint
|
||||||
|
}
|
||||||
|
smokeTests["reachability"] = reachability
|
||||||
|
}
|
||||||
|
if config.SmokeTests.Authentication != nil {
|
||||||
|
authentication := make(map[string]any)
|
||||||
|
if config.SmokeTests.Authentication.Kind != nil {
|
||||||
|
authentication["kind"] = *config.SmokeTests.Authentication.Kind
|
||||||
|
}
|
||||||
|
if config.SmokeTests.Authentication.Endpoint != nil {
|
||||||
|
authentication["endpoint"] = *config.SmokeTests.Authentication.Endpoint
|
||||||
|
}
|
||||||
|
smokeTests["authentication"] = authentication
|
||||||
|
}
|
||||||
|
fields["smoke_tests"] = smokeTests
|
||||||
|
}
|
||||||
|
definitions = append(definitions, fields)
|
||||||
|
}
|
||||||
|
return configkit.Mapping(label, map[string]any{"backends": definitions})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalBackend supplies endpoint and concurrency pointers for a caller-chosen
|
||||||
|
// backend ID; it does not select an ID or validate either value.
|
||||||
|
func LocalBackend(endpoint string, concurrencyLimit int) BackendConfig {
|
||||||
|
return BackendConfig{Endpoint: &endpoint, ConcurrencyLimit: &concurrencyLimit}
|
||||||
}
|
}
|
||||||
|
|||||||
115
bundled_composition_contract_test.go
Normal file
115
bundled_composition_contract_test.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBundledCompositionReplacementAndInheritance(t *testing.T) {
|
||||||
|
underlay := configkit.Input{Source: configkit.Mapping("caller underlay", map[string]any{
|
||||||
|
"profiles": []any{map[string]any{"id": "gpt-sol-latest", "model": "underlay-model"}},
|
||||||
|
})}
|
||||||
|
underlayEngine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{underlay}}}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
underlayProfile, err := underlayEngine.InspectProfile(context.Background(), "gpt-sol-latest")
|
||||||
|
if err != nil || underlayProfile.EffectiveModelParams.Model != "underlay-model" || underlayProfile.EffectiveModelParams.BackendID != "" {
|
||||||
|
t.Fatalf("underlay replacement = %+v, %v", underlayProfile, err)
|
||||||
|
}
|
||||||
|
main := configkit.Input{Source: configkit.Mapping("caller main", map[string]any{
|
||||||
|
"backends": []any{map[string]any{"id": "openrouter", "endpoint": "https://local.invalid/v1"}, map[string]any{"id": "other", "endpoint": "https://other.invalid/v1"}},
|
||||||
|
"profiles": []any{
|
||||||
|
map[string]any{"id": "gpt-sol-latest", "model": "main-model"},
|
||||||
|
map[string]any{"id": "custom", "extends": map[string]any{"id": "gpt-luna-latest"}, "model": "custom-model"},
|
||||||
|
map[string]any{"id": "auto", "backend": "other", "model": "other-model"},
|
||||||
|
},
|
||||||
|
"collections": map[string]any{"openrouter": map[string]any{
|
||||||
|
"backends": []any{map[string]any{"id": "local", "endpoint": "https://collection.invalid/v1"}},
|
||||||
|
"profiles": []any{map[string]any{"id": "profile", "backend": "openrouter/local", "model": "collection-model"}},
|
||||||
|
}},
|
||||||
|
})}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{underlay}}}, Main: &main}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
inspect := func(id string) promptkit.ExecutionTarget {
|
||||||
|
t.Helper()
|
||||||
|
profile, err := engine.InspectProfile(context.Background(), id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return profile.EffectiveModelParams
|
||||||
|
}
|
||||||
|
if target := inspect("gpt-sol-latest"); target.Model != "main-model" || target.BackendID != "" {
|
||||||
|
t.Fatalf("replacement = %+v", target)
|
||||||
|
}
|
||||||
|
if target := inspect("custom"); target.Model != "custom-model" || target.BackendID != "openrouter" || target.Endpoint != "https://local.invalid/v1" {
|
||||||
|
t.Fatalf("inherited = %+v", target)
|
||||||
|
}
|
||||||
|
if target := inspect("gpt-luna-latest"); target.Endpoint != "https://local.invalid/v1" || target.APIKeyEnv != "" {
|
||||||
|
t.Fatalf("backend replacement = %+v", target)
|
||||||
|
}
|
||||||
|
if target := inspect("openrouter/profile"); target.BackendID != "openrouter/local" || target.Model != "collection-model" {
|
||||||
|
t.Fatalf("collection = %+v", target)
|
||||||
|
}
|
||||||
|
for _, tier := range []string{"low", "medium", "high", "xhigh", "max"} {
|
||||||
|
target := inspect("auto-" + tier)
|
||||||
|
if target.BackendID != "other" || target.Model != "other-model" || !strings.Contains(fmt.Sprint(target.ExtraParams), "cost_tier:"+tier) || !strings.Contains(fmt.Sprint(target.ExtraParams), "id:auto-router") {
|
||||||
|
t.Errorf("tier %s = %+v", tier, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundledCompositionSequenceModes(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name, mode, id string
|
||||||
|
wantErr bool
|
||||||
|
count int
|
||||||
|
}{
|
||||||
|
{"duplicate append", "append", "auto", true, 0},
|
||||||
|
{"distinct append", "append", "custom", false, 18},
|
||||||
|
{"replace", "replace", "custom", false, 1},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
input := configkit.Input{Source: configkit.Mapping("sequence", map[string]any{
|
||||||
|
"config": map[string]any{"sequences": []any{map[string]any{"destination": []any{"profiles"}, "mode": test.mode}}},
|
||||||
|
"profiles": []any{map[string]any{"id": test.id, "model": "custom-model"}},
|
||||||
|
})}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}})
|
||||||
|
if test.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected duplicate rejection")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
profiles, err := engine.ListProfiles(context.Background())
|
||||||
|
if err != nil || len(profiles) != test.count {
|
||||||
|
t.Fatalf("profiles = %v, %v", profiles, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundledCompositionRejectsStrictConflictsAndMissingBackends(t *testing.T) {
|
||||||
|
profile := configkit.Input{Source: configkit.Mapping("profile", map[string]any{"profiles": []any{map[string]any{"id": "auto", "model": "caller"}}})}
|
||||||
|
_, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{profile, profile}}}}})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("strict group accepted duplicate records")
|
||||||
|
}
|
||||||
|
input := configkit.Input{Source: configkit.Mapping("replace backends", map[string]any{
|
||||||
|
"config": map[string]any{"sequences": []any{map[string]any{"destination": []any{"backends"}, "mode": "replace"}}},
|
||||||
|
"backends": []any{map[string]any{"id": "other", "endpoint": "https://other.invalid/v1"}},
|
||||||
|
})}
|
||||||
|
if _, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}}); err == nil {
|
||||||
|
t.Fatal("missing bundled backend was accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
90
bundled_execution_contract_test.go
Normal file
90
bundled_execution_contract_test.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func bundledPromptConfig(client *http.Client) promptkit.Config {
|
||||||
|
input := configkit.Input{Source: configkit.Mapping("prompt", map[string]any{"prompts": []any{map[string]any{
|
||||||
|
"id": "prompt", "messages": []any{map[string]any{"role": "user", "content": "hello"}}, "output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
|
}}})}
|
||||||
|
return promptkit.Config{HTTPClient: client, Sources: promptkit.ConfigurationSources{Main: &input}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundledProfilePreparationAndExecution(t *testing.T) {
|
||||||
|
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}}
|
||||||
|
engine, err := promptkit.NewEngine(bundledPromptConfig(nil), promptkit.WithLLMClient(client))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := engine.PrepareExecution(context.Background(), promptkit.RunRequest{PromptID: "prompt"}); !errors.Is(err, promptkit.ErrProfileRequired) {
|
||||||
|
t.Fatalf("missing profile = %v", err)
|
||||||
|
}
|
||||||
|
temperature := 0.3
|
||||||
|
request := promptkit.RunRequest{PromptID: "prompt", ProfileID: "gpt-luna-latest", Execution: &promptkit.ExecutionTargetOverride{Temperature: &temperature}}
|
||||||
|
prepared, err := engine.PrepareExecution(context.Background(), request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
details := prepared.Details()
|
||||||
|
if details.SelectedProfileID != "gpt-luna-latest" || details.EffectiveModelParams.Model != "~openai/gpt-luna-latest" || details.EffectiveModelParams.BackendID != "openrouter" || details.EffectiveModelParams.Temperature != temperature {
|
||||||
|
t.Fatalf("prepared target = %+v", details.EffectiveModelParams)
|
||||||
|
}
|
||||||
|
if _, err := engine.Run(context.Background(), request); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(client.requests) != 1 || client.requests[0].Target.Model != "~openai/gpt-luna-latest" || client.requests[0].Target.BackendID != "openrouter" {
|
||||||
|
t.Fatalf("requests = %+v", client.requests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundledProfileHTTPPayloads(t *testing.T) {
|
||||||
|
t.Setenv("OPENROUTER_API_KEY", "synthetic-key")
|
||||||
|
for _, test := range []struct{ id, model, tier string }{
|
||||||
|
{"auto-low", "openrouter/auto", "low"}, {"auto-medium", "openrouter/auto", "medium"}, {"auto-high", "openrouter/auto", "high"}, {"auto-xhigh", "openrouter/auto", "xhigh"}, {"auto-max", "openrouter/auto", "max"},
|
||||||
|
{"auto", "openrouter/auto", ""}, {"free", "openrouter/free", ""}, {"gpt-luna-latest", "~openai/gpt-luna-latest", ""},
|
||||||
|
} {
|
||||||
|
t.Run(test.id, func(t *testing.T) {
|
||||||
|
var body map[string]any
|
||||||
|
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
||||||
|
if request.Header.Get("Authorization") != "Bearer synthetic-key" {
|
||||||
|
t.Errorf("authorization = %q", request.Header.Get("Authorization"))
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"choices":[{"message":{"content":"ok"}}]}`))}, nil
|
||||||
|
})
|
||||||
|
engine, err := promptkit.NewEngine(bundledPromptConfig(&http.Client{Transport: transport}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := engine.Run(context.Background(), promptkit.RunRequest{PromptID: "prompt", ProfileID: test.id}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if body["model"] != test.model || body["extra_params"] != nil {
|
||||||
|
t.Fatalf("request = %+v", body)
|
||||||
|
}
|
||||||
|
if test.tier == "" {
|
||||||
|
if _, exists := body["plugins"]; exists {
|
||||||
|
t.Fatalf("unexpected plugins = %+v", body)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
want := []any{map[string]any{"id": "auto-router", "cost_tier": test.tier}}
|
||||||
|
if !reflect.DeepEqual(body["plugins"], want) {
|
||||||
|
t.Fatalf("plugins = %#v, want %#v", body["plugins"], want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
124
bundled_openrouter_contract_test.go
Normal file
124
bundled_openrouter_contract_test.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rejectBundleTransport struct{}
|
||||||
|
|
||||||
|
func (rejectBundleTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||||
|
return nil, errors.New("unexpected HTTP request")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundledOpenRouterOptionsAndInventory(t *testing.T) {
|
||||||
|
t.Setenv("OPENROUTER_API_KEY", "")
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
options []promptkit.Option
|
||||||
|
enabled bool
|
||||||
|
}{
|
||||||
|
{"default", nil, true}, {"enabled", []promptkit.Option{promptkit.WithBundledOpenRouter(true)}, true},
|
||||||
|
{"disabled", []promptkit.Option{promptkit.WithBundledOpenRouter(false)}, false},
|
||||||
|
{"last disables", []promptkit.Option{promptkit.WithBundledOpenRouter(true), promptkit.WithBundledOpenRouter(false)}, false},
|
||||||
|
{"last enables", []promptkit.Option{promptkit.WithBundledOpenRouter(false), promptkit.WithBundledOpenRouter(true)}, true},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{HTTPClient: &http.Client{Transport: rejectBundleTransport{}}}, test.options...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
profiles, err := engine.ListProfiles(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
backends, err := engine.ListBackends(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
prompts, err := engine.ListPrompts(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
collections, err := engine.ListCollections(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := 0
|
||||||
|
if test.enabled {
|
||||||
|
want = 17
|
||||||
|
}
|
||||||
|
if len(profiles) != want || len(backends) != boolCount(test.enabled) || len(prompts) != 0 || len(collections) != 0 {
|
||||||
|
t.Fatalf("inventories: %v %v %v %v", profiles, backends, prompts, collections)
|
||||||
|
}
|
||||||
|
if test.enabled {
|
||||||
|
wantProfiles := []string{"auto", "auto-high", "auto-low", "auto-max", "auto-medium", "auto-xhigh", "claude-fable-latest", "claude-haiku-latest", "claude-opus-latest", "claude-sonnet-latest", "free", "gemini-flash-latest", "gemini-pro-latest", "gpt-astra-latest", "gpt-luna-latest", "gpt-sol-latest", "gpt-terra-latest"}
|
||||||
|
if !reflect.DeepEqual(profiles, wantProfiles) || !reflect.DeepEqual(backends, []string{"openrouter"}) {
|
||||||
|
t.Fatalf("bundle inventory: %v %v", profiles, backends)
|
||||||
|
}
|
||||||
|
for id, model := range map[string]string{"auto": "openrouter/auto", "auto-max": "openrouter/auto", "gpt-luna-latest": "~openai/gpt-luna-latest", "free": "openrouter/free"} {
|
||||||
|
profile, err := engine.InspectProfile(context.Background(), id)
|
||||||
|
if err != nil || profile.EffectiveModelParams.Model != model || profile.EffectiveModelParams.BackendID != "openrouter" {
|
||||||
|
t.Fatalf("profile %s = %+v, %v", id, profile, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(fmtSources(view.Sources()), "Promptkit bundled OpenRouter") {
|
||||||
|
t.Fatal("bundle source missing")
|
||||||
|
}
|
||||||
|
if explanation, found := view.Explain(configkit.Keys("backends").Index(0).Key("endpoint")); !found || !strings.Contains(explanation.Effective.YAML, "https://openrouter.ai/api/v1") || len(explanation.History) == 0 {
|
||||||
|
t.Fatalf("bundle explanation = %+v", explanation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolCount(value bool) int {
|
||||||
|
if value {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
func fmtSources(sources []configkit.SourceInfo) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for _, source := range sources {
|
||||||
|
b.WriteString(source.Label)
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundlePreservesCallerUnderlayBackingArray(t *testing.T) {
|
||||||
|
source := configkit.Mapping("caller", map[string]any{"profiles": []any{map[string]any{"id": "caller", "model": "model"}}})
|
||||||
|
groups := make([]configkit.Underlay, 1, 2)
|
||||||
|
groups[0] = configkit.Underlay{Inputs: []configkit.Input{{Source: source}}}
|
||||||
|
before := append([]configkit.Underlay(nil), groups...)
|
||||||
|
if _, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: groups}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(groups, before) || !reflect.DeepEqual(groups[:2][1], configkit.Underlay{}) {
|
||||||
|
t.Fatal("caller underlays were modified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisabledBundleKeepsCallerOpenRouter(t *testing.T) {
|
||||||
|
input := configkit.Input{Source: configkit.Mapping("caller", map[string]any{"backends": []any{map[string]any{"id": "openrouter", "endpoint": "https://local.invalid/v1"}}})}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}}, promptkit.WithBundledOpenRouter(false))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
backends, err := engine.ListBackends(context.Background())
|
||||||
|
if err != nil || !reflect.DeepEqual(backends, []string{"openrouter"}) {
|
||||||
|
t.Fatalf("backends = %v, %v", backends, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
127
bundled_smoke_contract_test.go
Normal file
127
bundled_smoke_contract_test.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBundledSmokeHTTPDefaults(t *testing.T) {
|
||||||
|
t.Setenv("OPENROUTER_API_KEY", "synthetic-key")
|
||||||
|
var urls []string
|
||||||
|
var models []string
|
||||||
|
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
||||||
|
urls = append(urls, request.URL.String())
|
||||||
|
body := `{"data":{"label":"member","is_free_tier":false,"usage":0}}`
|
||||||
|
if request.Method == http.MethodPost {
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
models = append(models, payload["model"].(string))
|
||||||
|
content := "PROMPTKIT_OK"
|
||||||
|
if _, structured := payload["response_format"]; structured {
|
||||||
|
content = `{"status":"ok"}`
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(map[string]any{"choices": []any{map[string]any{"message": map[string]any{"content": content}}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
body = string(encoded)
|
||||||
|
} else if strings.HasSuffix(request.URL.Path, "/key") && request.Header.Get("Authorization") != "Bearer synthetic-key" {
|
||||||
|
t.Errorf("authentication header missing")
|
||||||
|
}
|
||||||
|
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body))}, nil
|
||||||
|
})
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{HTTPClient: &http.Client{Transport: transport}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
before, _ := engine.ListProfiles(context.Background())
|
||||||
|
inspectionBefore, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, kind := range []promptkit.SmokeTestKind{promptkit.SmokeTestReachability, promptkit.SmokeTestAuthentication, promptkit.SmokeTestTextInference, promptkit.SmokeTestStructuredOutput} {
|
||||||
|
report, err := engine.SmokeTest(context.Background(), promptkit.SmokeTestRequest{Kind: kind, BackendID: "openrouter"})
|
||||||
|
if err != nil || report == nil || !report.Completed || report.Failure != "" {
|
||||||
|
t.Fatalf("%s report = %+v, %v", kind, report, err)
|
||||||
|
}
|
||||||
|
if kind == promptkit.SmokeTestAuthentication && (report.Facts.CredentialsAccepted == nil || !*report.Facts.CredentialsAccepted) {
|
||||||
|
t.Fatalf("authentication facts = %+v", report.Facts)
|
||||||
|
}
|
||||||
|
if kind == promptkit.SmokeTestTextInference || kind == promptkit.SmokeTestStructuredOutput {
|
||||||
|
if report.Target.ProfileID != "gpt-luna-latest" || report.Target.EffectiveSettings.Model != "~openai/gpt-luna-latest" {
|
||||||
|
t.Fatalf("target = %+v", report.Target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wantURLs := []string{"https://openrouter.ai/api/v1", "https://openrouter.ai/api/v1/key", "https://openrouter.ai/api/v1/chat/completions", "https://openrouter.ai/api/v1/chat/completions"}
|
||||||
|
if !reflect.DeepEqual(urls, wantURLs) || !reflect.DeepEqual(models, []string{"~openai/gpt-luna-latest", "~openai/gpt-luna-latest"}) {
|
||||||
|
t.Fatalf("urls %v, models %v", urls, models)
|
||||||
|
}
|
||||||
|
after, _ := engine.ListProfiles(context.Background())
|
||||||
|
if !reflect.DeepEqual(before, after) {
|
||||||
|
t.Fatalf("inventory changed: %v %v", before, after)
|
||||||
|
}
|
||||||
|
inspectionAfter, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
diff, err := inspectionBefore.Diff(inspectionAfter)
|
||||||
|
if err != nil || len(diff) != 0 {
|
||||||
|
t.Fatalf("inspection changed: %+v, %v", diff, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundledSmokeSelectionUsesEffectiveConfiguration(t *testing.T) {
|
||||||
|
replacement := configkit.Input{Source: configkit.Mapping("replacement", map[string]any{"profiles": []any{map[string]any{"id": "gpt-luna-latest", "backend": "openrouter", "model": "replacement"}}})}
|
||||||
|
replacementClient := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "PROMPTKIT_OK"}}
|
||||||
|
replacementEngine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &replacement}}, promptkit.WithLLMClient(replacementClient))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
replacementReport, err := replacementEngine.SmokeTest(context.Background(), promptkit.SmokeTestRequest{Kind: promptkit.SmokeTestTextInference, BackendID: "openrouter"})
|
||||||
|
if err != nil || replacementReport.Target.ProfileID != "gpt-luna-latest" || replacementReport.Target.EffectiveSettings.Model != "replacement" {
|
||||||
|
t.Fatalf("replaced default = %+v, %v", replacementReport, err)
|
||||||
|
}
|
||||||
|
replacementClient.response.Content = "unexpected"
|
||||||
|
mismatch, err := replacementEngine.SmokeTest(context.Background(), promptkit.SmokeTestRequest{Kind: promptkit.SmokeTestTextInference, BackendID: "openrouter"})
|
||||||
|
if err != nil || mismatch == nil || !mismatch.Completed {
|
||||||
|
t.Fatalf("concluded mismatch = %+v, %v", mismatch, err)
|
||||||
|
}
|
||||||
|
input := configkit.Input{Source: configkit.Mapping("application", map[string]any{
|
||||||
|
"backends": []any{map[string]any{"id": "openrouter", "endpoint": "https://example.invalid/v1", "smoke_tests": map[string]any{"default_profile": "custom"}}},
|
||||||
|
"profiles": []any{map[string]any{"id": "gpt-luna-latest", "backend": "openrouter", "model": "replacement"}, map[string]any{"id": "custom", "backend": "openrouter", "model": "custom-model"}},
|
||||||
|
"prompts": []any{map[string]any{"id": "prompt", "default_profile": "free", "messages": []any{map[string]any{"role": "user", "content": "private application marker"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}}},
|
||||||
|
})}
|
||||||
|
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "PROMPTKIT_OK"}}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}}, promptkit.WithLLMClient(client))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, test := range []struct {
|
||||||
|
request promptkit.SmokeTestRequest
|
||||||
|
profile, model string
|
||||||
|
}{
|
||||||
|
{promptkit.SmokeTestRequest{BackendID: "openrouter"}, "custom", "custom-model"},
|
||||||
|
{promptkit.SmokeTestRequest{BackendID: "openrouter", PromptID: "prompt"}, "free", "openrouter/free"},
|
||||||
|
{promptkit.SmokeTestRequest{BackendID: "openrouter", PromptID: "prompt", ProfileID: "gpt-luna-latest"}, "gpt-luna-latest", "replacement"},
|
||||||
|
} {
|
||||||
|
test.request.Kind = promptkit.SmokeTestTextInference
|
||||||
|
report, err := engine.SmokeTest(context.Background(), test.request)
|
||||||
|
if err != nil || report.Target.ProfileID != test.profile || report.Target.EffectiveSettings.Model != test.model {
|
||||||
|
t.Fatalf("report = %+v, %v", report, err)
|
||||||
|
}
|
||||||
|
if strings.Contains(client.requests[len(client.requests)-1].Prompt.Messages[0].Content, "private application marker") {
|
||||||
|
t.Fatal("application content used by diagnostic")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit"
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEngineLimitsInjectedClientConcurrency(t *testing.T) {
|
func TestEngineLimitsInjectedClientConcurrency(t *testing.T) {
|
||||||
@@ -150,17 +151,7 @@ func TestUnlimitedBackendsRetainInjectedClientConcurrency(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "endpoint-only profile",
|
name: "endpoint-only profile",
|
||||||
configure: func(t *testing.T, client promptkit.LLMClient) *promptkit.Engine {
|
configure: func(t *testing.T, client promptkit.LLMClient) *promptkit.Engine {
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{},
|
return newSimpleEngine(t, "message", promptkit.WithLLMClient(client))
|
||||||
promptkit.WithPromptFS(contractPromptFS("prompt", "profile", "message"), "."),
|
|
||||||
promptkit.WithProfiles(promptkit.Profile{
|
|
||||||
ID: "profile", Endpoint: "http://endpoint.example/v1", Model: "model",
|
|
||||||
}),
|
|
||||||
promptkit.WithLLMClient(client),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("construct endpoint-only engine: %v", err)
|
|
||||||
}
|
|
||||||
return engine
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -250,27 +241,36 @@ func newBackendCapacityEngine(
|
|||||||
reader promptkit.ArtifactReader,
|
reader promptkit.ArtifactReader,
|
||||||
) *promptkit.Engine {
|
) *promptkit.Engine {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
promptFS := contractPromptFS("prompt", "profile", "message")
|
promptID, content := "prompt", "message"
|
||||||
|
var inputs []any
|
||||||
if reader != nil {
|
if reader != nil {
|
||||||
promptFS = contractInputPromptFS()
|
promptID, content = "input-prompt", `{{input "input"}}`
|
||||||
|
inputs = []any{map[string]any{"name": "input", "required": true}}
|
||||||
}
|
}
|
||||||
options := []promptkit.Option{
|
backend := map[string]any{"id": "limited", "endpoint": "http://backend.example/v1", "concurrency_limit": limit}
|
||||||
promptkit.WithPromptFS(promptFS, "."),
|
if queueCapacity != nil {
|
||||||
promptkit.WithBackend(promptkit.Backend{
|
backend["queue_capacity"] = *queueCapacity
|
||||||
ID: "limited",
|
|
||||||
Endpoint: "http://backend.example/v1",
|
|
||||||
ConcurrencyLimit: limit,
|
|
||||||
QueueCapacity: queueCapacity,
|
|
||||||
}),
|
|
||||||
promptkit.WithProfiles(promptkit.Profile{
|
|
||||||
ID: "profile", BackendID: "limited", Model: "model",
|
|
||||||
}),
|
|
||||||
promptkit.WithLLMClient(client),
|
|
||||||
}
|
}
|
||||||
|
definition := map[string]any{
|
||||||
|
"id": promptID,
|
||||||
|
"default_profile": "profile",
|
||||||
|
"messages": []any{map[string]any{"role": "user", "content": content}},
|
||||||
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
|
}
|
||||||
|
if inputs != nil {
|
||||||
|
definition["inputs"] = inputs
|
||||||
|
}
|
||||||
|
options := []promptkit.Option{promptkit.WithLLMClient(client)}
|
||||||
if reader != nil {
|
if reader != nil {
|
||||||
options = append(options, promptkit.WithArtifactReader(reader))
|
options = append(options, promptkit.WithArtifactReader(reader))
|
||||||
}
|
}
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{}, options...)
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
||||||
|
Source: configkit.Mapping("capacity fixture", map[string]any{
|
||||||
|
"backends": []any{backend},
|
||||||
|
"profiles": []any{map[string]any{"id": "profile", "backend": "limited", "model": "model"}},
|
||||||
|
"prompts": []any{definition},
|
||||||
|
}),
|
||||||
|
}}}, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("construct capacity engine: %v", err)
|
t.Fatalf("construct capacity engine: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
261
configuration_diff_contract_test.go
Normal file
261
configuration_diff_contract_test.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigurationInspectionDiffsCapturedValues(t *testing.T) {
|
||||||
|
left := comparisonInspection(t, "earlier-model")
|
||||||
|
right := comparisonInspection(t, "later-model")
|
||||||
|
|
||||||
|
differences, err := left.Diff(right)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("compare captured configurations: %v", err)
|
||||||
|
}
|
||||||
|
if len(differences) != 1 {
|
||||||
|
t.Fatalf("difference count = %d, want 1: %+v", len(differences), differences)
|
||||||
|
}
|
||||||
|
difference := differences[0]
|
||||||
|
if got, want := difference.Path.String(), "/profiles"; got != want {
|
||||||
|
t.Fatalf("difference path = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if !strings.Contains(difference.Left.YAML, "earlier-model") || !strings.Contains(difference.Right.YAML, "later-model") {
|
||||||
|
t.Fatalf("difference snapshots = (%q, %q)", difference.Left.YAML, difference.Right.YAML)
|
||||||
|
}
|
||||||
|
|
||||||
|
reversed, err := right.Diff(left)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("compare reversed configurations: %v", err)
|
||||||
|
}
|
||||||
|
if len(reversed) != 1 || reversed[0].Path.String() != difference.Path.String() ||
|
||||||
|
!strings.Contains(reversed[0].Left.YAML, "later-model") ||
|
||||||
|
!strings.Contains(reversed[0].Right.YAML, "earlier-model") {
|
||||||
|
t.Fatalf("reversed difference = %+v", reversed)
|
||||||
|
}
|
||||||
|
|
||||||
|
equal := comparisonInspection(t, "earlier-model")
|
||||||
|
if equal, err := left.Diff(equal); err != nil || len(equal) != 0 {
|
||||||
|
t.Fatalf("compare equal configurations = (%+v, %v), want no differences", equal, err)
|
||||||
|
}
|
||||||
|
if equal, err := left.Diff(left); err != nil || len(equal) != 0 {
|
||||||
|
t.Fatalf("compare view with itself = (%+v, %v), want no differences", equal, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigurationInspectionDiffRejectsZeroViews(t *testing.T) {
|
||||||
|
valid := comparisonInspection(t, "current-model")
|
||||||
|
var zero promptkit.ConfigurationInspection
|
||||||
|
|
||||||
|
for _, comparison := range []struct {
|
||||||
|
name string
|
||||||
|
left, right promptkit.ConfigurationInspection
|
||||||
|
}{
|
||||||
|
{name: "zero left", left: zero, right: valid},
|
||||||
|
{name: "zero right", left: valid, right: zero},
|
||||||
|
{name: "both zero", left: zero, right: zero},
|
||||||
|
} {
|
||||||
|
t.Run(comparison.name, func(t *testing.T) {
|
||||||
|
differences, err := comparison.left.Diff(comparison.right)
|
||||||
|
if !errors.Is(err, promptkit.ErrInvalidConfig) || len(differences) != 0 {
|
||||||
|
t.Fatalf("Diff() = (%+v, %v), want ErrInvalidConfig and no records", differences, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigurationInspectionDiffIgnoresSourceHistory(t *testing.T) {
|
||||||
|
left := comparisonInspectionFrom(t, "earlier source", "same-model", nil)
|
||||||
|
right := comparisonInspectionFrom(t, "later source", "same-model", nil)
|
||||||
|
if !hasSourceLabel(left, "earlier source") || !hasSourceLabel(right, "later source") {
|
||||||
|
t.Fatal("comparison fixtures did not retain their distinct source labels")
|
||||||
|
}
|
||||||
|
|
||||||
|
differences, err := left.Diff(right)
|
||||||
|
if err != nil || len(differences) != 0 {
|
||||||
|
t.Fatalf("compare equal effective values with different sources = (%+v, %v), want no differences", differences, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigurationInspectionDiffUsesNormalizedValues(t *testing.T) {
|
||||||
|
inspect := func(endpoint string) promptkit.ConfigurationInspection {
|
||||||
|
t.Helper()
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("backend", map[string]any{
|
||||||
|
"backends": []any{map[string]any{"id": "local", "endpoint": endpoint}},
|
||||||
|
})}}}}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
view, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
left := inspect(" HTTPS://example.invalid/v1 ")
|
||||||
|
equal := inspect("https://example.invalid/v1")
|
||||||
|
differences, err := left.Diff(equal)
|
||||||
|
if err != nil || len(differences) != 0 {
|
||||||
|
t.Fatalf("normalized diff = %+v, %v", differences, err)
|
||||||
|
}
|
||||||
|
changed := inspect("https://other.invalid/v1")
|
||||||
|
differences, err = left.Diff(changed)
|
||||||
|
if err != nil || len(differences) != 1 || differences[0].Path.String() != "/backends" {
|
||||||
|
t.Fatalf("changed diff = %+v, %v", differences, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigurationInspectionDiffReportsPresenceAndSequences(t *testing.T) {
|
||||||
|
left := comparisonInspectionFrom(t, "before", "same-model", map[string]any{
|
||||||
|
"sequence": []any{"earlier", "unchanged"},
|
||||||
|
})
|
||||||
|
right := comparisonInspectionFrom(t, "after", "same-model", map[string]any{
|
||||||
|
"nullable": nil,
|
||||||
|
"sequence": []any{"later", "unchanged"},
|
||||||
|
})
|
||||||
|
|
||||||
|
differences, err := left.Diff(right)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("compare presence and sequence values: %v", err)
|
||||||
|
}
|
||||||
|
if len(differences) != 1 || differences[0].Path.String() != "/backends" {
|
||||||
|
t.Fatalf("atomic backend list difference = %+v", differences)
|
||||||
|
}
|
||||||
|
difference := differences[0]
|
||||||
|
if !difference.Left.Present || !difference.Right.Present ||
|
||||||
|
strings.Contains(difference.Left.YAML, "nullable:") || !strings.Contains(difference.Right.YAML, "nullable: null") ||
|
||||||
|
!strings.Contains(difference.Left.YAML, "earlier") || !strings.Contains(difference.Right.YAML, "later") {
|
||||||
|
t.Fatalf("atomic sequence snapshots = %+v", difference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigurationInspectionDiffProtectsSensitiveValuesAndReturnedRecords(t *testing.T) {
|
||||||
|
left := comparisonInspectionFrom(t, "before", "same-model", map[string]any{
|
||||||
|
"token": "left-secret-sentinel",
|
||||||
|
"visible": "before-value",
|
||||||
|
})
|
||||||
|
right := comparisonInspectionFrom(t, "after", "same-model", map[string]any{
|
||||||
|
"token": "right-secret-sentinel",
|
||||||
|
"visible": "after-value",
|
||||||
|
})
|
||||||
|
|
||||||
|
differences, err := left.Diff(right)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("compare sensitive values: %v", err)
|
||||||
|
}
|
||||||
|
if len(differences) != 1 || differences[0].Path.String() != "/backends" {
|
||||||
|
t.Fatalf("sensitive backend list difference = %+v", differences)
|
||||||
|
}
|
||||||
|
secret := differences[0]
|
||||||
|
if !secret.Left.Redacted || !secret.Right.Redacted || !secret.HiddenChanged {
|
||||||
|
t.Fatalf("sensitive difference = %+v", secret)
|
||||||
|
}
|
||||||
|
if strings.Contains(secret.Left.YAML, "left-secret-sentinel") || strings.Contains(secret.Left.YAML, "right-secret-sentinel") ||
|
||||||
|
strings.Contains(secret.Right.YAML, "left-secret-sentinel") || strings.Contains(secret.Right.YAML, "right-secret-sentinel") {
|
||||||
|
t.Fatalf("sensitive snapshots disclosed a value: %+v", secret)
|
||||||
|
}
|
||||||
|
if !strings.Contains(secret.Left.YAML, "before-value") || !strings.Contains(secret.Right.YAML, "after-value") {
|
||||||
|
t.Fatalf("public sibling missing from snapshots: %+v", secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
differences[0].Path = configkit.Keys("caller", "mutation")
|
||||||
|
differences[0].Left.YAML = "caller mutation"
|
||||||
|
differences[0].Right.YAML = "caller mutation"
|
||||||
|
again, err := left.Diff(right)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("repeat comparison after mutating returned records: %v", err)
|
||||||
|
}
|
||||||
|
if len(again) != 1 {
|
||||||
|
t.Fatalf("repeat difference count = %d, want 1: %+v", len(again), again)
|
||||||
|
}
|
||||||
|
againByPath := differencesByPath(again)
|
||||||
|
if _, exists := againByPath["/caller/mutation"]; exists {
|
||||||
|
t.Fatal("mutating returned path reached retained inspection")
|
||||||
|
}
|
||||||
|
if _, exists := againByPath["/backends"]; !exists {
|
||||||
|
t.Fatalf("original backend path was lost after mutation: %+v", again)
|
||||||
|
}
|
||||||
|
for _, difference := range again {
|
||||||
|
if difference.Left.YAML == "caller mutation" || difference.Right.YAML == "caller mutation" {
|
||||||
|
t.Fatalf("mutating returned snapshots reached retained inspection: %+v", difference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigurationInspectionDiffMasksSensitiveShapeReplacement(t *testing.T) {
|
||||||
|
left := comparisonInspectionFrom(t, "before", "same-model", map[string]any{
|
||||||
|
"details": map[string]any{
|
||||||
|
"token": "nested-secret-sentinel",
|
||||||
|
"label": "public detail",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
right := comparisonInspectionFrom(t, "after", "same-model", map[string]any{
|
||||||
|
"details": "replacement",
|
||||||
|
})
|
||||||
|
|
||||||
|
differences, err := left.Diff(right)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("compare sensitive shape replacement: %v", err)
|
||||||
|
}
|
||||||
|
if len(differences) != 1 || differences[0].Path.String() != "/backends" {
|
||||||
|
t.Fatalf("shape replacement differences = %+v", differences)
|
||||||
|
}
|
||||||
|
difference := differences[0]
|
||||||
|
if !difference.Left.Redacted || !difference.Right.Redacted || !difference.HiddenChanged {
|
||||||
|
t.Fatalf("shape replacement masking = %+v", difference)
|
||||||
|
}
|
||||||
|
if strings.Contains(difference.Left.YAML, "nested-secret-sentinel") || strings.Contains(difference.Right.YAML, "nested-secret-sentinel") {
|
||||||
|
t.Fatalf("shape replacement disclosed a sensitive value: %+v", difference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func comparisonInspection(t *testing.T, model string) promptkit.ConfigurationInspection {
|
||||||
|
t.Helper()
|
||||||
|
return comparisonInspectionFrom(t, "comparison fixture", model, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func comparisonInspectionFrom(t *testing.T, source, model string, extraParams map[string]any) promptkit.ConfigurationInspection {
|
||||||
|
t.Helper()
|
||||||
|
backend := map[string]any{"id": "local", "endpoint": "https://example.invalid/v1"}
|
||||||
|
if extraParams != nil {
|
||||||
|
backend["extra_params"] = extraParams
|
||||||
|
}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping(source, map[string]any{
|
||||||
|
"backends": []any{backend},
|
||||||
|
"profiles": []any{map[string]any{
|
||||||
|
"id": "offline", "backend": "local",
|
||||||
|
"model": model,
|
||||||
|
}},
|
||||||
|
})}}}},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct comparison engine: %v", err)
|
||||||
|
}
|
||||||
|
inspection, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("inspect comparison engine: %v", err)
|
||||||
|
}
|
||||||
|
return inspection
|
||||||
|
}
|
||||||
|
|
||||||
|
func differencesByPath(differences []configkit.Difference) map[string]configkit.Difference {
|
||||||
|
byPath := make(map[string]configkit.Difference, len(differences))
|
||||||
|
for _, difference := range differences {
|
||||||
|
byPath[difference.Path.String()] = difference
|
||||||
|
}
|
||||||
|
return byPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasSourceLabel(view promptkit.ConfigurationInspection, label string) bool {
|
||||||
|
for _, source := range view.Sources() {
|
||||||
|
if source.Label == label {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
99
configuration_inspection.go
Normal file
99
configuration_inspection.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package promptkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit/internal/configuration"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigurationInspection is a read-only view of one Engine's captured
|
||||||
|
// effective configuration and source history. It does not expose typed
|
||||||
|
// configuration values, filesystem contexts, or runtime credentials.
|
||||||
|
// Report content may include application prompt and provider data, so callers
|
||||||
|
// choose where and to whom to disclose it.
|
||||||
|
type ConfigurationInspection struct {
|
||||||
|
source configuration.Inspection
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectConfiguration returns the ConfigKit inspection view captured by this
|
||||||
|
// Engine. It does not reload sources, resolve credentials, or contact a model.
|
||||||
|
// A nil Engine returns ErrInvalidConfig.
|
||||||
|
func (e *Engine) InspectConfiguration() (ConfigurationInspection, error) {
|
||||||
|
if e == nil || e.internal == nil {
|
||||||
|
return ConfigurationInspection{}, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
||||||
|
}
|
||||||
|
return ConfigurationInspection{source: e.internal.InspectConfiguration()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diff compares this view's captured effective configuration with other. This
|
||||||
|
// view is the earlier (left) value and other is the later (right) value. The
|
||||||
|
// comparison delegates to ConfigKit's frozen effective-value semantics, so
|
||||||
|
// source or resolution history alone does not create differences. It does not
|
||||||
|
// reread external sources or compare referenced file contents, resolved
|
||||||
|
// credential contents, later execution preparation, or request-specific
|
||||||
|
// overrides. Either zero view returns ErrInvalidConfig and no records; other
|
||||||
|
// ConfigKit errors are returned unchanged. An empty diff is not evidence of
|
||||||
|
// equivalent engine behavior or execution. Records can contain ordinary
|
||||||
|
// application data, so callers choose where and to whom to disclose them.
|
||||||
|
func (v ConfigurationInspection) Diff(other ConfigurationInspection) ([]configkit.Difference, error) {
|
||||||
|
if v.source == nil || other.source == nil {
|
||||||
|
return nil, ErrInvalidConfig
|
||||||
|
}
|
||||||
|
return configuration.Diff(v.source, other.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata returns detached effective presence and history records.
|
||||||
|
func (v ConfigurationInspection) Metadata() configkit.Metadata {
|
||||||
|
if v.source == nil {
|
||||||
|
return configkit.Metadata{}
|
||||||
|
}
|
||||||
|
return v.source.Metadata()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sources returns a detached list of loaded source descriptions.
|
||||||
|
func (v ConfigurationInspection) Sources() []configkit.SourceInfo {
|
||||||
|
if v.source == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v.source.Sources()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagnostics returns detached nonfatal load diagnostics.
|
||||||
|
func (v ConfigurationInspection) Diagnostics() []configkit.Diagnostic {
|
||||||
|
if v.source == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v.source.Diagnostics()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explain returns detached effective and historical values for a known path.
|
||||||
|
// Dynamic credential-shaped provider keys are redacted by ConfigKit; ordinary
|
||||||
|
// prompt and provider content is not automatically safe to disclose.
|
||||||
|
func (v ConfigurationInspection) Explain(path configkit.Path) (configkit.Explanation, bool) {
|
||||||
|
if v.source == nil {
|
||||||
|
return configkit.Explanation{}, false
|
||||||
|
}
|
||||||
|
return v.source.Explain(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteEffective writes ConfigKit's redacted effective YAML. Writer errors
|
||||||
|
// remain available through errors.Is and errors.As. The zero view returns
|
||||||
|
// ErrInvalidConfig.
|
||||||
|
func (v ConfigurationInspection) WriteEffective(w io.Writer) error {
|
||||||
|
if v.source == nil {
|
||||||
|
return ErrInvalidConfig
|
||||||
|
}
|
||||||
|
return v.source.WriteEffective(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteExplanation writes ConfigKit's redacted history for path. Unknown paths
|
||||||
|
// produce no output; writer errors remain inspectable. The zero view returns
|
||||||
|
// ErrInvalidConfig.
|
||||||
|
func (v ConfigurationInspection) WriteExplanation(w io.Writer, path configkit.Path) error {
|
||||||
|
if v.source == nil {
|
||||||
|
return ErrInvalidConfig
|
||||||
|
}
|
||||||
|
return v.source.WriteExplanation(w, path)
|
||||||
|
}
|
||||||
321
configuration_inspection_contract_test.go
Normal file
321
configuration_inspection_contract_test.go
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigurationInspectionReportsLiteralKeysAndRedactedHistory(t *testing.T) {
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{
|
||||||
|
{Inputs: []configkit.Input{{Source: configkit.Mapping("defaults", map[string]any{
|
||||||
|
"backends": []any{map[string]any{
|
||||||
|
"id": "local", "endpoint": " HTTPS://example.invalid/v1 ", "api_key_env": "OPTIONAL_KEY",
|
||||||
|
"extra_params": map[string]any{"token": "private-sentinel", "visible": "public-value"},
|
||||||
|
}},
|
||||||
|
"collections": map[string]any{"literal": map[string]any{
|
||||||
|
"backends": []any{
|
||||||
|
map[string]any{"id": "local", "endpoint": "https://example.invalid/v1", "extra_params": map[string]any{"token": "namespaced-private-sentinel"}},
|
||||||
|
map[string]any{"id": "child", "extends": map[string]any{"id": "local"}},
|
||||||
|
},
|
||||||
|
"profiles": []any{
|
||||||
|
map[string]any{"id": "base.v1", "backend": "local", "model": "default-model"},
|
||||||
|
map[string]any{"id": "id", "extends": map[string]any{"id": "base.v1"}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
})}}},
|
||||||
|
{Inputs: []configkit.Input{{Source: configkit.Mapping("operator", map[string]any{
|
||||||
|
"collections": map[string]any{"literal": map[string]any{
|
||||||
|
"backends": []any{map[string]any{"id": "local", "endpoint": "https://example.invalid/v1", "extra_params": map[string]any{"token": "replacement-private-sentinel"}}},
|
||||||
|
"profiles": []any{map[string]any{"id": "base.v1", "backend": "local", "model": "operator-model"}},
|
||||||
|
}},
|
||||||
|
})}}},
|
||||||
|
},
|
||||||
|
}}, promptkit.WithBundledOpenRouter(false))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct layered engine: %v", err)
|
||||||
|
}
|
||||||
|
view, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("inspect configuration: %v", err)
|
||||||
|
}
|
||||||
|
modelPath := configkit.Keys("collections", "literal", "profiles").Index(0).Key("model")
|
||||||
|
model, found := view.Explain(modelPath)
|
||||||
|
if !found || !strings.Contains(model.Effective.YAML, "operator-model") || len(model.History) == 0 {
|
||||||
|
t.Fatalf("layered model explanation = (%+v, %t)", model, found)
|
||||||
|
}
|
||||||
|
inheritedPath := configkit.Keys("collections", "literal", "profiles").Index(1).Key("model")
|
||||||
|
inherited, found := view.Explain(inheritedPath)
|
||||||
|
if !found || !strings.Contains(inherited.Effective.YAML, "operator-model") || len(inherited.History) == 0 || !view.Metadata().IsExplicit(inheritedPath) {
|
||||||
|
t.Fatalf("literal-key inheritance explanation = (%+v, %t)", inherited, found)
|
||||||
|
}
|
||||||
|
if inheritedPath.String() != "/collections/literal/profiles/1/model" {
|
||||||
|
t.Fatalf("literal-key path = %q", inheritedPath)
|
||||||
|
}
|
||||||
|
secretPath := configkit.Keys("backends").Index(0).Key("extra_params").Key("token")
|
||||||
|
endpointPath := configkit.Keys("backends").Index(0).Key("endpoint")
|
||||||
|
endpoint, found := view.Explain(endpointPath)
|
||||||
|
if !found || !strings.Contains(endpoint.Effective.YAML, "https://example.invalid/v1") || strings.Contains(fmt.Sprintf("%+v", view.Metadata().History(endpointPath)), "private-sentinel") {
|
||||||
|
t.Fatalf("normalized endpoint explanation = %+v", endpoint)
|
||||||
|
}
|
||||||
|
secret, found := view.Explain(secretPath)
|
||||||
|
if !found || !secret.Effective.Redacted || strings.Contains(secret.Effective.YAML, "private-sentinel") {
|
||||||
|
t.Fatalf("credential-shaped provider value was not redacted: (%+v, %t)", secret, found)
|
||||||
|
}
|
||||||
|
namespacedSecretPath := configkit.Keys("collections", "literal", "backends").Index(0).Key("extra_params").Key("token")
|
||||||
|
namespacedSecret, found := view.Explain(namespacedSecretPath)
|
||||||
|
if !found || !namespacedSecret.Effective.Redacted || strings.Contains(namespacedSecret.Effective.YAML, "namespaced-private-sentinel") {
|
||||||
|
t.Fatalf("namespaced provider value was not redacted: (%+v, %t)", namespacedSecret, found)
|
||||||
|
}
|
||||||
|
inheritedSecretPath := configkit.Keys("collections", "literal", "backends").Index(1).Key("extra_params").Key("token")
|
||||||
|
inheritedSecret, found := view.Explain(inheritedSecretPath)
|
||||||
|
if !found || !inheritedSecret.Effective.Redacted || strings.Contains(inheritedSecret.Effective.YAML, "replacement-private-sentinel") {
|
||||||
|
t.Fatalf("inherited provider value was not redacted: (%+v, %t)", inheritedSecret, found)
|
||||||
|
}
|
||||||
|
metadata := fmt.Sprintf("%+v", view.Metadata().History(inheritedSecretPath))
|
||||||
|
if strings.Contains(metadata, "replacement-private-sentinel") || strings.Contains(metadata, "namespaced-private-sentinel") {
|
||||||
|
t.Fatal("metadata disclosed a sensitive provider value")
|
||||||
|
}
|
||||||
|
var effective bytes.Buffer
|
||||||
|
if err := view.WriteEffective(&effective); err != nil {
|
||||||
|
t.Fatalf("write effective configuration: %v", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(effective.String(), "private-sentinel") || !strings.Contains(effective.String(), "OPTIONAL_KEY") || !strings.Contains(effective.String(), "public-value") {
|
||||||
|
t.Fatalf("unexpected effective report: %s", effective.String())
|
||||||
|
}
|
||||||
|
var explanation bytes.Buffer
|
||||||
|
if err := view.WriteExplanation(&explanation, namespacedSecretPath); err != nil || strings.Contains(explanation.String(), "namespaced-private-sentinel") {
|
||||||
|
t.Fatalf("secret explanation = (%q, %v)", explanation.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sources := view.Sources()
|
||||||
|
if len(sources) == 0 {
|
||||||
|
t.Fatal("inspection returned no sources")
|
||||||
|
}
|
||||||
|
originalLabel := sources[0].Label
|
||||||
|
sources[0].Label = "changed"
|
||||||
|
if view.Sources()[0].Label != originalLabel {
|
||||||
|
t.Fatal("source mutation reached retained inspection")
|
||||||
|
}
|
||||||
|
model.History[0].After.YAML = "changed"
|
||||||
|
if again, ok := view.Explain(modelPath); !ok || again.History[0].After.YAML == "changed" {
|
||||||
|
t.Fatal("explanation mutation reached retained inspection")
|
||||||
|
}
|
||||||
|
history := view.Metadata().History(modelPath)
|
||||||
|
if len(history) == 0 {
|
||||||
|
t.Fatal("metadata history is empty")
|
||||||
|
}
|
||||||
|
history[0].Reasons = []string{"changed"}
|
||||||
|
if again := view.Metadata().History(modelPath); len(again) == 0 || (len(again[0].Reasons) > 0 && again[0].Reasons[0] == "changed") {
|
||||||
|
t.Fatal("metadata mutation reached retained inspection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigurationInspectionFailureDoesNotDiscloseSensitiveValues(t *testing.T) {
|
||||||
|
_, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
||||||
|
Source: configkit.Mapping("invalid backend", map[string]any{
|
||||||
|
"collections": map[string]any{"private": map[string]any{"backends": []any{
|
||||||
|
map[string]any{"id": "backend", "endpoint": "invalid endpoint", "extra_params": map[string]any{"token": "failure-private-sentinel"}},
|
||||||
|
}}},
|
||||||
|
}),
|
||||||
|
}}})
|
||||||
|
var loadErr *configkit.LoadError
|
||||||
|
if !errors.As(err, &loadErr) {
|
||||||
|
t.Fatalf("invalid backend error = %v, want load error", err)
|
||||||
|
}
|
||||||
|
if text := fmt.Sprintf("%v %+v", err, loadErr.Diagnostics()); strings.Contains(text, "failure-private-sentinel") {
|
||||||
|
t.Fatalf("failure diagnostics disclosed a sensitive value: %s", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigurationInspectionRetainsMainSpecificationDeclaration(t *testing.T) {
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
||||||
|
Source: configkit.Mapping("main", map[string]any{"version": "2"}),
|
||||||
|
}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine with accepted declaration: %v", err)
|
||||||
|
}
|
||||||
|
inspection, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("inspect configuration: %v", err)
|
||||||
|
}
|
||||||
|
path := configkit.Keys("version")
|
||||||
|
field, found := inspection.Explain(path)
|
||||||
|
if !found || !inspection.Metadata().IsExplicit(path) || !strings.Contains(field.Effective.YAML, "2") {
|
||||||
|
t.Fatalf("main specification declaration was not retained: (%+v, %t)", field, found)
|
||||||
|
}
|
||||||
|
var effective bytes.Buffer
|
||||||
|
if err := inspection.WriteEffective(&effective); err != nil || !strings.Contains(effective.String(), "version") {
|
||||||
|
t.Fatalf("effective inspection = (%q, %v), want retained version", effective.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type failingInspectionWriter struct{ err error }
|
||||||
|
|
||||||
|
func (writer failingInspectionWriter) Write([]byte) (int, error) { return 0, writer.err }
|
||||||
|
|
||||||
|
func TestConfigurationInspectionZeroAndWriterErrors(t *testing.T) {
|
||||||
|
var engine *promptkit.Engine
|
||||||
|
if _, err := engine.InspectConfiguration(); !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||||||
|
t.Fatalf("nil engine inspection = %v", err)
|
||||||
|
}
|
||||||
|
var empty promptkit.ConfigurationInspection
|
||||||
|
path := configkit.Keys("profiles", "missing")
|
||||||
|
if _, found := empty.Metadata().Lookup(path); found || len(empty.Sources()) != 0 || len(empty.Diagnostics()) != 0 {
|
||||||
|
t.Fatal("zero inspection reported configuration")
|
||||||
|
}
|
||||||
|
if _, found := empty.Explain(path); found {
|
||||||
|
t.Fatal("zero inspection explained an unknown path")
|
||||||
|
}
|
||||||
|
if err := empty.WriteEffective(io.Discard); !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||||||
|
t.Fatalf("zero effective writer = %v", err)
|
||||||
|
}
|
||||||
|
if err := empty.WriteExplanation(io.Discard, path); !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||||||
|
t.Fatalf("zero explanation writer = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
profile := configkit.Input{Source: configkit.Mapping("inspection test profile", map[string]any{
|
||||||
|
"profiles": []any{map[string]any{"id": "inspection-profile", "endpoint": "https://configured.example/v1", "model": "configured-model"}},
|
||||||
|
})}
|
||||||
|
valid, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{profile}}},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
view, err := valid.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("inspect configuration: %v", err)
|
||||||
|
}
|
||||||
|
writeErr := errors.New("report writer failed")
|
||||||
|
writer := failingInspectionWriter{err: writeErr}
|
||||||
|
if err := view.WriteEffective(writer); !errors.Is(err, writeErr) {
|
||||||
|
t.Fatalf("effective writer error = %v", err)
|
||||||
|
}
|
||||||
|
if err := view.WriteExplanation(writer, configkit.Keys("profiles").Index(0).Key("model")); !errors.Is(err, writeErr) {
|
||||||
|
t.Fatalf("explanation writer error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigurationInspectionUsesCapturedSourcesWithoutProviderCalls(t *testing.T) {
|
||||||
|
const promptDocument = "prompts:\n - id: captured\n default_profile: offline\n messages:\n - role: user\n content_file: body.tmpl\n output:\n format: text\n validation_mode: none\n"
|
||||||
|
source := &unreadableAfterConstructionFS{FS: fstest.MapFS{
|
||||||
|
"config/prompt.yaml": &fstest.MapFile{Data: []byte(promptDocument)},
|
||||||
|
"config/body.tmpl": &fstest.MapFile{Data: []byte("hello")},
|
||||||
|
}}
|
||||||
|
inputs, err := promptkit.FilesystemInputs("inspection source", source, "config")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("select inputs: %v", err)
|
||||||
|
}
|
||||||
|
otherSource := &unreadableAfterConstructionFS{FS: fstest.MapFS{
|
||||||
|
"config/prompt.yaml": &fstest.MapFile{Data: []byte(promptDocument)},
|
||||||
|
"config/body.tmpl": &fstest.MapFile{Data: []byte("hello")},
|
||||||
|
}}
|
||||||
|
otherInputs, err := promptkit.FilesystemInputs("comparison source", otherSource, "config")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("select comparison inputs: %v", err)
|
||||||
|
}
|
||||||
|
var modelCalls atomic.Int64
|
||||||
|
client := countingLLMClient{calls: &modelCalls}
|
||||||
|
endpoint, model, otherModel := "https://example.invalid/v1", "offline-model", "comparison-model"
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: inputs}, {Inputs: []configkit.Input{{Source: configkit.Mapping("offline profile", map[string]any{
|
||||||
|
"profiles": []any{map[string]any{"id": "offline", "endpoint": endpoint, "model": model}},
|
||||||
|
})}}}},
|
||||||
|
}}, promptkit.WithLLMClient(client))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
otherEngine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: otherInputs}, {Inputs: []configkit.Input{{Source: configkit.Mapping("comparison profile", map[string]any{
|
||||||
|
"profiles": []any{map[string]any{"id": "offline", "endpoint": endpoint, "model": otherModel}},
|
||||||
|
})}}}},
|
||||||
|
}}, promptkit.WithLLMClient(client))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct comparison engine: %v", err)
|
||||||
|
}
|
||||||
|
source.closed.Store(true)
|
||||||
|
otherSource.closed.Store(true)
|
||||||
|
view, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("inspect configuration: %v", err)
|
||||||
|
}
|
||||||
|
otherView, err := otherEngine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("inspect comparison configuration: %v", err)
|
||||||
|
}
|
||||||
|
var expected configkit.Difference
|
||||||
|
for index := 0; index < 3; index++ {
|
||||||
|
differences, err := view.Diff(otherView)
|
||||||
|
if err != nil || len(differences) != 1 {
|
||||||
|
t.Fatalf("compare captured configurations = (%+v, %v), want one difference", differences, err)
|
||||||
|
}
|
||||||
|
if differences[0].Path.String() != "/profiles" ||
|
||||||
|
!strings.Contains(differences[0].Left.YAML, model) ||
|
||||||
|
!strings.Contains(differences[0].Right.YAML, otherModel) {
|
||||||
|
t.Fatalf("captured difference = %+v", differences[0])
|
||||||
|
}
|
||||||
|
if index == 0 {
|
||||||
|
expected = differences[0]
|
||||||
|
} else if differences[0].Path.String() != expected.Path.String() ||
|
||||||
|
differences[0].Left.YAML != expected.Left.YAML || differences[0].Right.YAML != expected.Right.YAML {
|
||||||
|
t.Fatalf("captured difference changed between reads: first=%+v next=%+v", expected, differences[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, found := view.Explain(configkit.Keys("prompts").Index(0).Key("messages").Index(0).Key("content_file")); !found {
|
||||||
|
t.Fatal("captured prompt content path was not inspectable")
|
||||||
|
}
|
||||||
|
var report bytes.Buffer
|
||||||
|
if err := view.WriteEffective(&report); err != nil || !strings.Contains(report.String(), "captured") {
|
||||||
|
t.Fatalf("write captured report = (%q, %v)", report.String(), err)
|
||||||
|
}
|
||||||
|
if source.reads.Load() != 0 || otherSource.reads.Load() != 0 || modelCalls.Load() != 0 {
|
||||||
|
t.Fatalf("inspection or comparison touched source or provider: reads=(%d,%d) requests=%d", source.reads.Load(), otherSource.reads.Load(), modelCalls.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
const calls = 24
|
||||||
|
var wait sync.WaitGroup
|
||||||
|
errs := make(chan error, calls)
|
||||||
|
for index := 0; index < calls; index++ {
|
||||||
|
wait.Add(1)
|
||||||
|
go func(index int) {
|
||||||
|
defer wait.Done()
|
||||||
|
switch index % 3 {
|
||||||
|
case 0:
|
||||||
|
_, err := engine.InspectConfiguration()
|
||||||
|
errs <- err
|
||||||
|
case 1:
|
||||||
|
_, err := engine.Run(context.Background(), promptkit.RunRequest{PromptID: "captured", SessionID: "direct"})
|
||||||
|
errs <- err
|
||||||
|
case 2:
|
||||||
|
differences, err := view.Diff(otherView)
|
||||||
|
if err == nil && (len(differences) != 1 || differences[0].Path.String() != expected.Path.String() ||
|
||||||
|
differences[0].Left.YAML != expected.Left.YAML || differences[0].Right.YAML != expected.Right.YAML) {
|
||||||
|
err = errors.New("concurrent comparison returned an unexpected difference")
|
||||||
|
}
|
||||||
|
errs <- err
|
||||||
|
}
|
||||||
|
}(index)
|
||||||
|
}
|
||||||
|
wait.Wait()
|
||||||
|
close(errs)
|
||||||
|
for err := range errs {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if source.reads.Load() != 0 || otherSource.reads.Load() != 0 || modelCalls.Load() != calls/3 {
|
||||||
|
t.Fatalf("concurrent inspection/comparison/execution = source reads (%d,%d), model calls %d", source.reads.Load(), otherSource.reads.Load(), modelCalls.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
100
configuration_sources.go
Normal file
100
configuration_sources.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package promptkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit/internal/domain"
|
||||||
|
"go.maximumdirect.net/promptkit/internal/filecatalog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CollectionInput mounts source at the selected collection's configuration
|
||||||
|
// destination, collections/<namespace>, and enables ConfigKit imports for that
|
||||||
|
// input. An invalid namespace returns an error matching ErrInvalidConfig. The
|
||||||
|
// source is not read or validated until ConfigKit loads it. Callers may adjust
|
||||||
|
// the returned input, including its filesystem context or imports setting.
|
||||||
|
func CollectionInput(namespace string, source configkit.Source) (configkit.Input, error) {
|
||||||
|
if !domain.IsValidResourceNamespace(namespace) {
|
||||||
|
return configkit.Input{}, fmt.Errorf("%w: invalid collection namespace %q", ErrInvalidConfig, namespace)
|
||||||
|
}
|
||||||
|
return configkit.Input{
|
||||||
|
Source: source,
|
||||||
|
Destination: configkit.Keys("collections", namespace),
|
||||||
|
Imports: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirectoryInputs selects the YAML files beneath an explicitly chosen host
|
||||||
|
// directory as one strict-composition group. The returned order is deterministic;
|
||||||
|
// it does not assign precedence among those files. Discovery is recursive over
|
||||||
|
// .yaml and .yml files. An unavailable root fails; an empty directory yields
|
||||||
|
// no inputs. Imports are disabled unless callers enable them on the inputs.
|
||||||
|
func DirectoryInputs(directory string) ([]configkit.Input, error) {
|
||||||
|
root, err := filepath.Abs(directory)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("resolve configuration directory: %w", err)
|
||||||
|
}
|
||||||
|
info, err := os.Stat(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("inspect configuration directory: %w", err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
return nil, fmt.Errorf("configuration root is not a directory: %s", root)
|
||||||
|
}
|
||||||
|
root, err = filepath.EvalSymlinks(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("resolve configuration directory: %w", err)
|
||||||
|
}
|
||||||
|
files, err := filecatalog.FindYAMLFiles(context.Background(), root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("discover configuration files: %w", err)
|
||||||
|
}
|
||||||
|
inputs := make([]configkit.Input, 0, len(files))
|
||||||
|
for _, file := range files {
|
||||||
|
inputs = append(inputs, configkit.Input{
|
||||||
|
Source: configkit.File(file),
|
||||||
|
Filesystem: &configkit.FilesystemContext{Root: root},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return inputs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilesystemInputs selects the YAML files beneath root in a caller-owned
|
||||||
|
// filesystem as one strict-composition group. Discovery is recursive over
|
||||||
|
// .yaml and .yml files, with deterministic ordering and no precedence among
|
||||||
|
// files. The caller retains ownership of the filesystem; an unavailable root
|
||||||
|
// fails and an empty directory yields no inputs. Imports remain disabled unless
|
||||||
|
// callers enable them on inputs.
|
||||||
|
func FilesystemInputs(label string, filesystem fs.FS, root string) ([]configkit.Input, error) {
|
||||||
|
if filesystem == nil {
|
||||||
|
return nil, fmt.Errorf("configuration filesystem is nil")
|
||||||
|
}
|
||||||
|
root = filecatalog.CleanFSRoot(root)
|
||||||
|
if !fs.ValidPath(root) {
|
||||||
|
return nil, fmt.Errorf("configuration root is not a valid filesystem path: %s", root)
|
||||||
|
}
|
||||||
|
info, err := fs.Stat(filesystem, root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("inspect configuration root: %w", err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
return nil, fmt.Errorf("configuration root is not a directory: %s", root)
|
||||||
|
}
|
||||||
|
files, err := filecatalog.FindFSYAMLFiles(context.Background(), filesystem, root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("discover configuration files: %w", err)
|
||||||
|
}
|
||||||
|
namespace := configkit.NewFilesystem(label, filesystem)
|
||||||
|
inputs := make([]configkit.Input, 0, len(files))
|
||||||
|
for _, file := range files {
|
||||||
|
inputs = append(inputs, configkit.Input{
|
||||||
|
Source: namespace.File(file),
|
||||||
|
Filesystem: &configkit.FilesystemContext{FS: &namespace, Root: root},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return inputs, nil
|
||||||
|
}
|
||||||
157
configuration_sources_test.go
Normal file
157
configuration_sources_test.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sourceDocument struct {
|
||||||
|
First string `yaml:"first"`
|
||||||
|
Second string `yaml:"second"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectionInputValidatesWithoutReadingSource(t *testing.T) {
|
||||||
|
input, err := promptkit.CollectionInput("docs", configkit.File(filepath.Join(t.TempDir(), "not-read.yaml")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if input.Destination.String() != "/collections/docs" || !input.Imports {
|
||||||
|
t.Fatalf("collection input = %+v, want destination /collections/docs with imports enabled", input)
|
||||||
|
}
|
||||||
|
for _, namespace := range []string{"", " ", "docs/api"} {
|
||||||
|
if _, err := promptkit.CollectionInput(namespace, configkit.NoSource()); !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||||||
|
t.Errorf("namespace %q error = %v, want ErrInvalidConfig", namespace, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectionInputLoadsNativeImportsAndLaterOverride(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
mainPath := filepath.Join(directory, "collection.yaml")
|
||||||
|
defaultsPath := filepath.Join(directory, "defaults.yaml")
|
||||||
|
if err := os.WriteFile(mainPath, []byte("config:\n imports:\n - path: defaults.yaml\n required: true\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(defaultsPath, []byte("prompts:\n - id: greeting\n messages:\n - role: user\n content: Hello\n output:\n format: text\n validation_mode: none\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
input, err := promptkit.CollectionInput("docs", configkit.File(mainPath))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}},
|
||||||
|
Main: &configkit.Input{
|
||||||
|
Source: configkit.Mapping("later collection override", map[string]any{"prompts": []any{map[string]any{"id": "greeting", "inputs": []any{map[string]any{"name": "overridden"}}, "messages": []any{map[string]any{"role": "user", "content": "Updated"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}}}}),
|
||||||
|
Destination: configkit.Keys("collections", "docs"),
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine with native collection import: %v", err)
|
||||||
|
}
|
||||||
|
inspection, err := engine.InspectPrompt(context.Background(), "docs/greeting")
|
||||||
|
if err != nil || inspection.PromptID != "docs/greeting" || len(inspection.Inputs) != 1 || inspection.Inputs[0].Name != "overridden" {
|
||||||
|
t.Fatalf("inspect imported and overlaid prompt = (%+v, %v)", inspection, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectionInputLoadsEmbeddedImportsAndLaterOverride(t *testing.T) {
|
||||||
|
files := fstest.MapFS{
|
||||||
|
"collection.yaml": {Data: []byte("config:\n imports:\n - path: defaults.yaml\n required: true\n")},
|
||||||
|
"defaults.yaml": {Data: []byte("prompts:\n - id: greeting\n messages:\n - role: user\n content: Hello\n output:\n format: text\n validation_mode: none\n")},
|
||||||
|
}
|
||||||
|
filesystem := configkit.NewFilesystem("embedded collection", files)
|
||||||
|
input, err := promptkit.CollectionInput("docs", filesystem.File("collection.yaml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
input.Filesystem = &configkit.FilesystemContext{FS: &filesystem, Root: "."}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}},
|
||||||
|
Main: &configkit.Input{
|
||||||
|
Source: configkit.Mapping("later collection override", map[string]any{"prompts": []any{map[string]any{"id": "greeting", "inputs": []any{map[string]any{"name": "overridden"}}, "messages": []any{map[string]any{"role": "user", "content": "Updated"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}}}}),
|
||||||
|
Destination: configkit.Keys("collections", "docs"),
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine with embedded collection import: %v", err)
|
||||||
|
}
|
||||||
|
inspection, err := engine.InspectPrompt(context.Background(), "docs/greeting")
|
||||||
|
if err != nil || inspection.PromptID != "docs/greeting" || len(inspection.Inputs) != 1 || inspection.Inputs[0].Name != "overridden" {
|
||||||
|
t.Fatalf("inspect imported and overlaid prompt = (%+v, %v)", inspection, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirectoryInputsSelectsSortedFilesWithRootContext(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
if err := os.Mkdir(filepath.Join(root, "nested"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for name, data := range map[string]string{
|
||||||
|
"a.yaml": "first: one\n", "nested/b.yml": "second: two\n", "nested/body.md": "message",
|
||||||
|
} {
|
||||||
|
if err := os.WriteFile(filepath.Join(root, name), []byte(data), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inputs, err := promptkit.DirectoryInputs(root)
|
||||||
|
if err != nil || len(inputs) != 2 || inputs[0].Imports || inputs[1].Imports {
|
||||||
|
t.Fatalf("directory inputs: %+v, %v", inputs, err)
|
||||||
|
}
|
||||||
|
assertSourceInputs(t, inputs, "message")
|
||||||
|
if _, err := promptkit.DirectoryInputs(filepath.Join(root, "missing")); err == nil {
|
||||||
|
t.Fatal("missing selected directory succeeded")
|
||||||
|
}
|
||||||
|
if empty, err := promptkit.DirectoryInputs(t.TempDir()); err != nil || len(empty) != 0 {
|
||||||
|
t.Fatalf("empty directory: %+v, %v", empty, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilesystemInputsSelectsEmbeddedFilesWithRootContext(t *testing.T) {
|
||||||
|
files := fstest.MapFS{
|
||||||
|
"settings/a.yaml": {Data: []byte("first: one\n")},
|
||||||
|
"settings/nested/b.yml": {Data: []byte("second: two\n")},
|
||||||
|
"settings/nested/body.md": {Data: []byte("message")},
|
||||||
|
}
|
||||||
|
inputs, err := promptkit.FilesystemInputs("embedded", files, "settings")
|
||||||
|
if err != nil || len(inputs) != 2 || inputs[0].Imports || inputs[1].Imports {
|
||||||
|
t.Fatalf("filesystem inputs: %+v, %v", inputs, err)
|
||||||
|
}
|
||||||
|
assertSourceInputs(t, inputs, "message")
|
||||||
|
if _, err := promptkit.FilesystemInputs("embedded", files, "missing"); err == nil {
|
||||||
|
t.Fatal("missing selected root succeeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertSourceInputs(t *testing.T, inputs []configkit.Input, expected string) {
|
||||||
|
t.Helper()
|
||||||
|
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[sourceDocument]{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: inputs}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.Config.First != "one" || result.Config.Second != "two" {
|
||||||
|
t.Fatalf("configuration files did not compose: %+v", result.Config)
|
||||||
|
}
|
||||||
|
field, found := result.Metadata().Lookup(configkit.Keys("second"))
|
||||||
|
if !found || len(field.Origin.Locations) != 1 {
|
||||||
|
t.Fatalf("missing second-field origin: %+v", field)
|
||||||
|
}
|
||||||
|
context, found := result.SourceContext(field.Origin.Locations[0].SourceID)
|
||||||
|
if !found || context.BaseDir != "nested" {
|
||||||
|
t.Fatalf("unexpected rooted source context: %+v", context)
|
||||||
|
}
|
||||||
|
data, err := fs.ReadFile(context.FS, "nested/body.md")
|
||||||
|
if err != nil || string(data) != expected {
|
||||||
|
t.Fatalf("rooted resource read: %q, %v", data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
317
construction_contract_test.go
Normal file
317
construction_contract_test.go
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProfileEnvironmentLookupAcceptsNonShellNames(t *testing.T) {
|
||||||
|
const name = "promptkit.test-api-key"
|
||||||
|
for _, test := range []struct {
|
||||||
|
name, value, direct, want string
|
||||||
|
unset, clear bool
|
||||||
|
}{
|
||||||
|
{name: "set", value: " synthetic-key ", want: "Bearer synthetic-key"},
|
||||||
|
{name: "empty"},
|
||||||
|
{name: "whitespace", value: " "},
|
||||||
|
{name: "unset", unset: true},
|
||||||
|
{name: "direct precedence", value: "synthetic-key", direct: "direct-key", want: "Bearer direct-key"},
|
||||||
|
{name: "explicit clear", value: "synthetic-key", clear: true},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
t.Setenv(name, test.value)
|
||||||
|
if test.unset {
|
||||||
|
if err := os.Unsetenv(name); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg := simpleConfig("hello")
|
||||||
|
cfg.Sources.Underlays = []configkit.Underlay{{Inputs: []configkit.Input{*cfg.Sources.Main}}}
|
||||||
|
endpoint, model := "http://example.test/v1", "model"
|
||||||
|
lowerName := name
|
||||||
|
cfg.Sources.Underlays = append(cfg.Sources.Underlays, configkit.Underlay{Inputs: []configkit.Input{{
|
||||||
|
Source: promptkit.ProfileSource("default credentials", []promptkit.ProfileConfig{
|
||||||
|
{ID: "profile", Endpoint: &endpoint, Model: &model, APIKeyEnv: &lowerName},
|
||||||
|
}),
|
||||||
|
}}})
|
||||||
|
env := " " + name + " "
|
||||||
|
if test.clear {
|
||||||
|
env = ""
|
||||||
|
}
|
||||||
|
cfg.Sources.Main = &configkit.Input{Source: promptkit.ProfileSource("credentials", []promptkit.ProfileConfig{
|
||||||
|
{ID: "profile", Endpoint: &endpoint, Model: &model, APIKeyEnv: &env},
|
||||||
|
})}
|
||||||
|
var calls int
|
||||||
|
var auth string
|
||||||
|
cfg.HTTPClient = &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
||||||
|
calls++
|
||||||
|
auth = request.Header.Get("Authorization")
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"choices":[{"message":{"content":"ok"}}]}`))}, nil
|
||||||
|
})}
|
||||||
|
engine, err := promptkit.NewEngine(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = engine.Run(context.Background(), promptkit.RunRequest{PromptID: "prompt", APIKey: test.direct})
|
||||||
|
if err != nil || calls != 1 || auth != test.want {
|
||||||
|
t.Fatalf("run: err=%v calls=%d authorization=%q, want %q", err, calls, auth, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicConstructionWithLocalBackendAndProgrammaticProfile(t *testing.T) {
|
||||||
|
backendID, model := "workstation", "offline-model"
|
||||||
|
prompt := configkit.Input{Source: configkit.Mapping("prompt", map[string]any{
|
||||||
|
"prompts": []any{map[string]any{
|
||||||
|
"id": "hello",
|
||||||
|
"default_profile": "offline",
|
||||||
|
"messages": []any{map[string]any{"role": "user", "content": "Hello"}},
|
||||||
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
|
}},
|
||||||
|
})}
|
||||||
|
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "reply"}}
|
||||||
|
localBackend := promptkit.LocalBackend("http://localhost:8000/v1", 2)
|
||||||
|
localBackend.ID = backendID
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{
|
||||||
|
{Source: promptkit.BackendSource("local backend", []promptkit.BackendConfig{
|
||||||
|
localBackend,
|
||||||
|
})},
|
||||||
|
{Source: promptkit.ProfileSource("profile", []promptkit.ProfileConfig{
|
||||||
|
{ID: "offline", Backend: &backendID, Model: &model},
|
||||||
|
})},
|
||||||
|
}}},
|
||||||
|
Main: &prompt,
|
||||||
|
}}, promptkit.WithLLMClient(client))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
result, err := engine.Run(context.Background(), promptkit.RunRequest{PromptID: "hello"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("run prompt: %v", err)
|
||||||
|
}
|
||||||
|
if result.RawOutput != "reply" || result.SelectedBackendID != backendID || result.EffectiveModelParams.Model != model {
|
||||||
|
t.Fatalf("unexpected result: %+v", result)
|
||||||
|
}
|
||||||
|
if len(client.requests) != 1 || client.requests[0].Target.Endpoint != "http://localhost:8000/v1" {
|
||||||
|
t.Fatalf("unexpected model request: %+v", client.requests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormerCatalogBackendIDsWorkWhenConfigured(t *testing.T) {
|
||||||
|
openRouterID, rakestrawhomeID := promptkit.BackendOpenRouter, promptkit.BackendRakestrawHome
|
||||||
|
openRouterEndpoint, rakestrawhomeEndpoint := "https://openrouter.example/v1", "https://inference.example/v1"
|
||||||
|
model := "configured-model"
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{
|
||||||
|
{Source: promptkit.BackendSource("application backends", []promptkit.BackendConfig{
|
||||||
|
{ID: openRouterID, Endpoint: &openRouterEndpoint},
|
||||||
|
{ID: rakestrawhomeID, Endpoint: &rakestrawhomeEndpoint},
|
||||||
|
})},
|
||||||
|
{Source: promptkit.ProfileSource("application profiles", []promptkit.ProfileConfig{
|
||||||
|
{ID: "openrouter-profile", Backend: &openRouterID, Model: &model},
|
||||||
|
{ID: "rakestrawhome-profile", Backend: &rakestrawhomeID, Model: &model},
|
||||||
|
})},
|
||||||
|
}}},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine with explicitly configured provider IDs: %v", err)
|
||||||
|
}
|
||||||
|
for profileID, backendID := range map[string]string{
|
||||||
|
"openrouter-profile": openRouterID,
|
||||||
|
"rakestrawhome-profile": rakestrawhomeID,
|
||||||
|
} {
|
||||||
|
inspection, err := engine.InspectProfile(context.Background(), profileID)
|
||||||
|
if err != nil || inspection.EffectiveModelParams.BackendID != backendID {
|
||||||
|
t.Fatalf("inspect %q = (%+v, %v), want backend %q", profileID, inspection, err, backendID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicConstructionCombinesEmbeddedOperatorAndProgrammaticInputs(t *testing.T) {
|
||||||
|
embedded, err := promptkit.FilesystemInputs("embedded defaults", fstest.MapFS{
|
||||||
|
"config/default.yaml": &fstest.MapFile{Data: []byte(`
|
||||||
|
profiles:
|
||||||
|
- id: application
|
||||||
|
endpoint: https://example.invalid/v1
|
||||||
|
model: default-model
|
||||||
|
prompts:
|
||||||
|
- id: hello
|
||||||
|
default_profile: application
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: Hello
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
`)},
|
||||||
|
}, "config")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("select embedded defaults: %v", err)
|
||||||
|
}
|
||||||
|
operatorDirectory := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(operatorDirectory, "override.yaml"), []byte("profiles:\n - id: application\n endpoint: https://example.invalid/v1\n model: operator-model\n"), 0o644); err != nil {
|
||||||
|
t.Fatalf("write operator input: %v", err)
|
||||||
|
}
|
||||||
|
operator, err := promptkit.DirectoryInputs(operatorDirectory)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("select operator inputs: %v", err)
|
||||||
|
}
|
||||||
|
programmaticModel, programmaticEndpoint := "programmatic-model", "https://example.invalid/v1"
|
||||||
|
programmatic := configkit.Input{Source: promptkit.ProfileSource("programmatic settings", []promptkit.ProfileConfig{
|
||||||
|
{ID: "application", Endpoint: &programmaticEndpoint, Model: &programmaticModel},
|
||||||
|
})}
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
apply bool
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "embedded", want: "default-model"},
|
||||||
|
{name: "operator", apply: true, want: "operator-model"},
|
||||||
|
{name: "programmatic", apply: true, want: "programmatic-model"},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
underlays := []configkit.Underlay{{Inputs: embedded}}
|
||||||
|
if test.apply {
|
||||||
|
underlays = append(underlays, configkit.Underlay{Inputs: operator})
|
||||||
|
}
|
||||||
|
if test.name == "programmatic" {
|
||||||
|
underlays = append(underlays, configkit.Underlay{Inputs: []configkit.Input{programmatic}})
|
||||||
|
}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: underlays}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "hello"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("prepare prompt: %v", err)
|
||||||
|
}
|
||||||
|
if prepared.EffectiveModelParams.Model != test.want {
|
||||||
|
t.Fatalf("model = %q, want %q", prepared.EffectiveModelParams.Model, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicConstructionPreservesConfigKitLoadError(t *testing.T) {
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
||||||
|
Source: configkit.Mapping("invalid", map[string]any{"profiles": []any{map[string]any{"id": "broken", "unknown": true}}}),
|
||||||
|
}}})
|
||||||
|
var loadErr *configkit.LoadError
|
||||||
|
if engine != nil || !errors.Is(err, promptkit.ErrInvalidConfig) || !errors.As(err, &loadErr) {
|
||||||
|
t.Fatalf("construction = (%#v, %v), want invalid config preserving LoadError", engine, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicConstructionRejectsSuppliedInvalidMainInput(t *testing.T) {
|
||||||
|
main := configkit.Input{}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &main}})
|
||||||
|
if engine != nil || !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||||||
|
t.Fatalf("construction = (%#v, %v), want invalid supplied main input", engine, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicConstructionUsesProgrammaticBackendAndProfileSources(t *testing.T) {
|
||||||
|
endpoint, backendID, model := "http://application.example/v1", "application-backend", "synthetic-model"
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{
|
||||||
|
{Source: promptkit.BackendSource("operator backend", []promptkit.BackendConfig{
|
||||||
|
{ID: backendID, Endpoint: &endpoint},
|
||||||
|
})},
|
||||||
|
{Source: promptkit.ProfileSource("operator profile", []promptkit.ProfileConfig{
|
||||||
|
{ID: "application-profile", Backend: &backendID, Model: &model},
|
||||||
|
})},
|
||||||
|
}}},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine with backend overlay: %v", err)
|
||||||
|
}
|
||||||
|
inspection, err := engine.InspectProfile(context.Background(), "application-profile")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("inspect configured profile: %v", err)
|
||||||
|
}
|
||||||
|
if inspection.EffectiveModelParams.Endpoint != endpoint {
|
||||||
|
t.Fatalf("effective endpoint = %q, want %q", inspection.EffectiveModelParams.Endpoint, endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackendSourceOwnsSmokeTestValuesAndDefersDefaultLookup(t *testing.T) {
|
||||||
|
backendEndpoint := "https://backend.example/v1"
|
||||||
|
defaultProfile := "not-yet-listed"
|
||||||
|
reachabilityEndpoint := " https://probe.example/ready?check=1 "
|
||||||
|
authenticationKind := " OPENROUTER_KEY "
|
||||||
|
authenticationEndpoint := "https://probe.example/key?scope=current"
|
||||||
|
source := promptkit.BackendSource("diagnostic backend", []promptkit.BackendConfig{
|
||||||
|
{
|
||||||
|
ID: "local",
|
||||||
|
Endpoint: &backendEndpoint,
|
||||||
|
SmokeTests: &promptkit.BackendSmokeTestConfig{
|
||||||
|
DefaultProfile: &defaultProfile,
|
||||||
|
Reachability: &promptkit.ReachabilityProbeConfig{Endpoint: &reachabilityEndpoint},
|
||||||
|
Authentication: &promptkit.AuthenticationProbeConfig{
|
||||||
|
Kind: &authenticationKind, Endpoint: &authenticationEndpoint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
backendEndpoint = "https://mutated.example/v1"
|
||||||
|
defaultProfile = "mutated-profile"
|
||||||
|
reachabilityEndpoint = "https://mutated.example/ready"
|
||||||
|
authenticationKind = "none"
|
||||||
|
authenticationEndpoint = "https://mutated.example/key"
|
||||||
|
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: source}}}},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine with dangling diagnostic default: %v", err)
|
||||||
|
}
|
||||||
|
inspection, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var effective bytes.Buffer
|
||||||
|
if err := inspection.WriteEffective(&effective); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
contents := effective.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
"https://backend.example/v1",
|
||||||
|
"not-yet-listed",
|
||||||
|
"https://probe.example/ready?check=1",
|
||||||
|
"openrouter_key",
|
||||||
|
"https://probe.example/key?scope=current",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(contents, want) {
|
||||||
|
t.Errorf("effective configuration does not contain %q:\n%s", want, contents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, unwanted := range []string{
|
||||||
|
"mutated.example",
|
||||||
|
"mutated-profile",
|
||||||
|
} {
|
||||||
|
if strings.Contains(contents, unwanted) {
|
||||||
|
t.Errorf("effective configuration unexpectedly contains %q:\n%s", unwanted, contents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreparationSelectsSinglePromptDefinitionByID(t *testing.T) {
|
||||||
|
engine := newSimpleEngine(t, "selected")
|
||||||
|
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "prompt"})
|
||||||
|
if err != nil || prepared == nil || len(prepared.Messages) != 1 || prepared.Messages[0].Content != "selected" {
|
||||||
|
t.Fatalf("prepare prompt by ID = (%+v, %v)", prepared, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
73
consumer_test_helpers_test.go
Normal file
73
consumer_test_helpers_test.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeLLMClient struct {
|
||||||
|
response *promptkit.GenerateResponse
|
||||||
|
responses []*promptkit.GenerateResponse
|
||||||
|
err error
|
||||||
|
requests []promptkit.GenerateRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeLLMClient) Generate(_ context.Context, request promptkit.GenerateRequest) (*promptkit.GenerateResponse, error) {
|
||||||
|
f.requests = append(f.requests, request)
|
||||||
|
if f.err != nil {
|
||||||
|
return nil, f.err
|
||||||
|
}
|
||||||
|
if len(f.responses) > 0 {
|
||||||
|
index := len(f.requests) - 1
|
||||||
|
if index >= len(f.responses) {
|
||||||
|
return nil, fmt.Errorf("no response configured for generation %d", index+1)
|
||||||
|
}
|
||||||
|
return f.responses[index], nil
|
||||||
|
}
|
||||||
|
return f.response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type countingLLMClient struct{ calls *atomic.Int64 }
|
||||||
|
|
||||||
|
func (client countingLLMClient) Generate(context.Context, promptkit.GenerateRequest) (*promptkit.GenerateResponse, error) {
|
||||||
|
if client.calls != nil {
|
||||||
|
client.calls.Add(1)
|
||||||
|
}
|
||||||
|
return &promptkit.GenerateResponse{Content: "ok"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { return f(request) }
|
||||||
|
|
||||||
|
// newSimpleEngine exercises the public constructor with a complete keyed
|
||||||
|
// document. Tests that need layering or resources select their own inputs.
|
||||||
|
func newSimpleEngine(t *testing.T, content string, options ...promptkit.Option) *promptkit.Engine {
|
||||||
|
t.Helper()
|
||||||
|
engine, err := promptkit.NewEngine(simpleConfig(content), options...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct consumer test engine: %v", err)
|
||||||
|
}
|
||||||
|
return engine
|
||||||
|
}
|
||||||
|
|
||||||
|
func simpleConfig(content string) promptkit.Config {
|
||||||
|
return promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Main: &configkit.Input{Source: configkit.Mapping("consumer test", map[string]any{
|
||||||
|
"profiles": []any{map[string]any{"id": "profile",
|
||||||
|
"endpoint": "http://example.test/v1", "model": "model",
|
||||||
|
}},
|
||||||
|
"prompts": []any{map[string]any{"id": "prompt",
|
||||||
|
"default_profile": "profile",
|
||||||
|
"messages": []any{map[string]any{"role": "user", "content": content}},
|
||||||
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
|
}},
|
||||||
|
})},
|
||||||
|
}}
|
||||||
|
}
|
||||||
16
convert.go
16
convert.go
@@ -5,8 +5,8 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
"go.maximumdirect.net/promptkit/internal/domain"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/jsonvalue"
|
"go.maximumdirect.net/promptkit/internal/jsonvalue"
|
||||||
)
|
)
|
||||||
|
|
||||||
func toDomainRunRequest(req RunRequest) (domain.RunRequest, error) {
|
func toDomainRunRequest(req RunRequest) (domain.RunRequest, error) {
|
||||||
@@ -25,7 +25,6 @@ func toDomainRunRequest(req RunRequest) (domain.RunRequest, error) {
|
|||||||
}
|
}
|
||||||
return domain.RunRequest{
|
return domain.RunRequest{
|
||||||
PromptID: req.PromptID,
|
PromptID: req.PromptID,
|
||||||
PromptVersion: req.PromptVersion,
|
|
||||||
ProfileID: req.ProfileID,
|
ProfileID: req.ProfileID,
|
||||||
SessionID: req.SessionID,
|
SessionID: req.SessionID,
|
||||||
APIKey: req.APIKey,
|
APIKey: req.APIKey,
|
||||||
@@ -73,7 +72,6 @@ func fromDomainPreparedRun(prepared *domain.PreparedRun) *PreparedRun {
|
|||||||
}
|
}
|
||||||
return &PreparedRun{
|
return &PreparedRun{
|
||||||
PromptID: prepared.PromptID,
|
PromptID: prepared.PromptID,
|
||||||
PromptVersion: prepared.PromptVersion,
|
|
||||||
PromptHash: prepared.PromptHash,
|
PromptHash: prepared.PromptHash,
|
||||||
SelectedProfileID: prepared.SelectedProfileID,
|
SelectedProfileID: prepared.SelectedProfileID,
|
||||||
SelectedBackendID: prepared.SelectedBackendID,
|
SelectedBackendID: prepared.SelectedBackendID,
|
||||||
@@ -100,7 +98,6 @@ func fromDomainRunResult(result *domain.RunResult) *RunResult {
|
|||||||
RawOutput: result.RawOutput,
|
RawOutput: result.RawOutput,
|
||||||
Validation: fromDomainValidationResult(result.Validation),
|
Validation: fromDomainValidationResult(result.Validation),
|
||||||
PromptID: result.PromptID,
|
PromptID: result.PromptID,
|
||||||
PromptVersion: result.PromptVersion,
|
|
||||||
PromptHash: result.PromptHash,
|
PromptHash: result.PromptHash,
|
||||||
SessionID: result.SessionID,
|
SessionID: result.SessionID,
|
||||||
RenderedPromptHash: result.RenderedPromptHash,
|
RenderedPromptHash: result.RenderedPromptHash,
|
||||||
@@ -211,6 +208,12 @@ func toDomainExecutionTargetOverride(override *ExecutionTargetOverride) (*domain
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fromDomainExecutionTarget(target domain.ExecutionTarget) ExecutionTarget {
|
func fromDomainExecutionTarget(target domain.ExecutionTarget) ExecutionTarget {
|
||||||
|
return fromDomainExecutionTargetWithOwnedParams(target, copyAnyMap(target.ExtraParams))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromDomainExecutionTargetWithOwnedParams projects scalar settings and takes
|
||||||
|
// ownership of extraParams, which the caller has already copied.
|
||||||
|
func fromDomainExecutionTargetWithOwnedParams(target domain.ExecutionTarget, extraParams map[string]any) ExecutionTarget {
|
||||||
return ExecutionTarget{
|
return ExecutionTarget{
|
||||||
BackendID: target.BackendID,
|
BackendID: target.BackendID,
|
||||||
Endpoint: target.Endpoint,
|
Endpoint: target.Endpoint,
|
||||||
@@ -222,7 +225,7 @@ func fromDomainExecutionTarget(target domain.ExecutionTarget) ExecutionTarget {
|
|||||||
ServiceTier: target.ServiceTier,
|
ServiceTier: target.ServiceTier,
|
||||||
ReasoningEffort: target.ReasoningEffort,
|
ReasoningEffort: target.ReasoningEffort,
|
||||||
APIKeyEnv: target.APIKeyEnv,
|
APIKeyEnv: target.APIKeyEnv,
|
||||||
ExtraParams: copyAnyMap(target.ExtraParams),
|
ExtraParams: extraParams,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +255,6 @@ func fromDomainPromptInspection(inspection *domain.PromptInspection) *PromptInsp
|
|||||||
}
|
}
|
||||||
return &PromptInspection{
|
return &PromptInspection{
|
||||||
PromptID: inspection.PromptID,
|
PromptID: inspection.PromptID,
|
||||||
PromptVersion: inspection.PromptVersion,
|
|
||||||
PromptHash: inspection.PromptHash,
|
PromptHash: inspection.PromptHash,
|
||||||
DefaultProfileID: inspection.DefaultProfileID,
|
DefaultProfileID: inspection.DefaultProfileID,
|
||||||
Inputs: inputs,
|
Inputs: inputs,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
"go.maximumdirect.net/promptkit/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestToDomainAppendedMessages(t *testing.T) {
|
func TestToDomainAppendedMessages(t *testing.T) {
|
||||||
|
|||||||
37
doc.go
37
doc.go
@@ -1,27 +1,32 @@
|
|||||||
// Package promptkit provides an embeddable engine for preparing and executing
|
// Package promptkit provides an embeddable engine for preparing and executing
|
||||||
// prompt-defined LLM workflows.
|
// prompt-defined LLM workflows.
|
||||||
//
|
//
|
||||||
// Applications construct an [Engine] with [NewEngine], select filesystem or
|
// Applications construct an [Engine] with [NewEngine], select ConfigKit
|
||||||
// in-memory sources and optional engine-scoped [Backend] registrations, and
|
// sources for prompts, profiles, and backends, and
|
||||||
// call [Engine.InspectPrompt], [Engine.InspectProfile], [Engine.Prepare],
|
// call [Engine.InspectConfiguration], [Engine.InspectPrompt],
|
||||||
// [Engine.PrepareExecution], [Engine.Run], or [Engine.RunPrepared]. Concrete
|
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.ListProfiles],
|
||||||
// registries, repositories, validators, and the built-in OpenAI-compatible
|
// [Engine.ListPrompts], [Engine.Prepare], [Engine.PrepareExecution],
|
||||||
// client remain internal implementation details.
|
// [Engine.Run], or [Engine.RunPrepared]. Concrete registries, repositories,
|
||||||
|
// validators, and the built-in OpenAI-compatible client remain internal
|
||||||
|
// implementation details.
|
||||||
//
|
//
|
||||||
// # Concurrency and ownership
|
// # Concurrency and ownership
|
||||||
//
|
//
|
||||||
// An Engine supports concurrent InspectPrompt, InspectProfile, Prepare,
|
// An Engine supports concurrent InspectConfiguration, InspectPrompt,
|
||||||
|
// InspectRequirements, InspectProfile, ListProfiles, ListPrompts, Prepare,
|
||||||
// PrepareExecution, Run, and RunPrepared calls. Engine-local backend policies
|
// PrepareExecution, Run, and RunPrepared calls. Engine-local backend policies
|
||||||
// bound admitted Run and RunPrepared calls and model generations where
|
// bound admitted Run and RunPrepared calls and model generations where
|
||||||
// configured, while different backend pools and unlimited backends continue
|
// configured, while different backend pools and unlimited backends continue
|
||||||
// independently. An injected [LLMClient] or [ArtifactReader] can therefore
|
// independently. An injected [LLMClient] or [ArtifactReader] can therefore
|
||||||
// still receive concurrent calls and must be safe for that use.
|
// still receive concurrent calls and must be safe for that use.
|
||||||
//
|
//
|
||||||
// NewEngine copies in-memory profiles and backend definitions. Prepare,
|
// ConfigKit captures the selected configuration when NewEngine constructs an
|
||||||
// PrepareExecution, and Run copy request maps, slices, pointer values, and
|
// Engine. Prepare, PrepareExecution, and Run copy request maps, slices, pointer
|
||||||
// JSON-compatible extra parameters before using them. InspectPrompt and
|
// values, and JSON-compatible extra parameters before using them. InspectPrompt,
|
||||||
// InspectProfile return copied inspection values. Returned values and values
|
// InspectRequirements, and InspectProfile return copied inspection values.
|
||||||
// passed to extension interfaces are likewise isolated from engine state.
|
// ListProfiles and ListPrompts return caller-owned identity slices.
|
||||||
|
// Returned values and values passed to extension interfaces are likewise
|
||||||
|
// isolated from engine state.
|
||||||
// Callers own those copies and may mutate them after the call that supplied or
|
// Callers own those copies and may mutate them after the call that supplied or
|
||||||
// returned them. [CapacityError] values are caller-owned and may be mutated
|
// returned them. [CapacityError] values are caller-owned and may be mutated
|
||||||
// without affecting engine state or another error. Immutable [GenerationError]
|
// without affecting engine state or another error. Immutable [GenerationError]
|
||||||
@@ -52,9 +57,11 @@
|
|||||||
// used by those values.
|
// used by those values.
|
||||||
//
|
//
|
||||||
// Construction, inspection, handle, and error values, including [Config],
|
// Construction, inspection, handle, and error values, including [Config],
|
||||||
// [Backend], [RunRequest], [ArtifactRef], [ExecutionTargetOverride], [Profile],
|
// [ConfigurationSources], [ConfigurationInspection], [BackendConfig],
|
||||||
// [OpenAICompatibleProfileConfig], [ProfileInspection],
|
// [ProfileConfig], [RunRequest],
|
||||||
// [PromptInputDefinition], [PromptInspection], [PreparedExecution],
|
// [ArtifactRef], [ExecutionTargetOverride], [ProfileInspection],
|
||||||
|
// [PromptIdentity], [PromptInputDefinition], [PromptInspection],
|
||||||
|
// [RequirementInspectionRequest], [RequirementInspection], [PreparedExecution],
|
||||||
// [CapacityError], [GenerationError], [RetryExhaustedError], and
|
// [CapacityError], [GenerationError], [RetryExhaustedError], and
|
||||||
// [ExecutionEvent], do not have stable JSON
|
// [ExecutionEvent], do not have stable JSON
|
||||||
// representations. Direct API keys are nevertheless excluded from JSON for
|
// representations. Direct API keys are nevertheless excluded from JSON for
|
||||||
|
|||||||
60
docs/adr/0001-configuration-boundary.md
Normal file
60
docs/adr/0001-configuration-boundary.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# 0001: Configuration Ownership At The ConfigKit Handoff
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted.
|
||||||
|
|
||||||
|
## Date
|
||||||
|
|
||||||
|
2026-09-30
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
PromptKit is adopting ConfigKit to standardize configuration semantics and
|
||||||
|
simplify its loading machinery. Configuration includes prompts, profiles, and
|
||||||
|
backend definitions contributed by maintained catalogs and consuming applications.
|
||||||
|
ConfigKit supplies loading, composition, precedence, inheritance, and inspection
|
||||||
|
capabilities. PromptKit supplies LLM-specific interpretation and execution.
|
||||||
|
|
||||||
|
A clear handoff assigns each library responsibility for the configuration it
|
||||||
|
owns and keeps validation aligned with the values an application actually uses.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
ConfigKit owns configuration processing up to the handoff of the final effective
|
||||||
|
configuration. PromptKit takes ownership at that handoff and applies its domain
|
||||||
|
interpretation and validation to the final configuration.
|
||||||
|
|
||||||
|
The handoff includes effective presence/null information and resource context
|
||||||
|
supplied by ConfigKit. PromptKit uses this information to interpret effective
|
||||||
|
settings and resolve referenced resources.
|
||||||
|
|
||||||
|
PromptKit passes through ConfigKit's inspection capabilities for explicit
|
||||||
|
operator use. Configuration history and source explanations serve that
|
||||||
|
inspection purpose; domain validation operates on the final effective values.
|
||||||
|
|
||||||
|
This boundary applies uniformly to application configuration and maintained
|
||||||
|
catalog configuration, including backend definitions.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
- **Independent source or catalog preflight validation:** preserves checks on
|
||||||
|
individual contributions, but duplicates configuration processing and evaluates
|
||||||
|
values that may be replaced before handoff. Raw-source and intermediate-state
|
||||||
|
validation are outside PromptKit's scope, including equivalent test-only checks.
|
||||||
|
- **History-based source auditing:** can identify overwritten contributions, but
|
||||||
|
couples domain enforcement to configuration history. Historical inspection is
|
||||||
|
an operator capability rather than a validation mechanism.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
PromptKit's configuration logic and tests focus on the final effective values,
|
||||||
|
their domain meaning, and resulting execution behavior. ConfigKit owns failures
|
||||||
|
arising during its processing, which PromptKit preserves through its error
|
||||||
|
boundary. Catalog maintainers own their repositories' publishing checks.
|
||||||
|
|
||||||
|
The adoption work routes backend configuration through the same handoff and
|
||||||
|
retires PromptKit's source-level and intermediate-configuration validation.
|
||||||
|
Effective resource interpretation and explicit operator inspection remain
|
||||||
|
supported. This concentrates configuration semantics in ConfigKit and reduces
|
||||||
|
duplicated machinery and maintenance in PromptKit.
|
||||||
231
docs/consumers/configuration-migration.md
Normal file
231
docs/consumers/configuration-migration.md
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
# Configuration And API Migration
|
||||||
|
|
||||||
|
This breaking pre-v1 minor migration adopts Promptkit's vanity module path,
|
||||||
|
changes the v0.16-era keyed resources to identified lists, adopts ConfigKit v1,
|
||||||
|
and removes prompt-version labels.
|
||||||
|
Promptkit installs a default-on bundled OpenRouter catalog; applications choose configuration
|
||||||
|
inputs explicitly, and execution selects one prompt by its exact ID. This guide
|
||||||
|
explains the shape changes and decisions an application must make; it is not an
|
||||||
|
automatic migration tool. The [framework format reference](../formats.md) owns
|
||||||
|
the current configuration contract, and the root package GoDoc owns exact API
|
||||||
|
signatures and errors.
|
||||||
|
|
||||||
|
## Update Go Imports
|
||||||
|
|
||||||
|
Promptkit's module and public package import path is now
|
||||||
|
`go.maximumdirect.net/promptkit`, replacing
|
||||||
|
`gitea.maximumdirect.net/eric/promptkit`. Update application, library, test, and
|
||||||
|
generated imports, then select a release using the new declaration:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get go.maximumdirect.net/promptkit@VERSION
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `VERSION` with a release that declares the vanity module path.
|
||||||
|
Earlier tags, including v0.16.0, retain their original module declaration and
|
||||||
|
use the former import path. No permanent `replace` directive is needed for the
|
||||||
|
cutover. Source browser, licensing, and hosted-release links continue to use
|
||||||
|
the Forge repository URLs, independently of Go's module identifier.
|
||||||
|
|
||||||
|
The package name remains `promptkit`. The path change itself does not rename
|
||||||
|
exported APIs or change their behavior, but Go treats packages imported through
|
||||||
|
the two paths as distinct. Update dependent packages that exchange Promptkit
|
||||||
|
types together and review any old-path dependencies retained after tidying.
|
||||||
|
The configuration and API adaptations below are also part of this migration.
|
||||||
|
|
||||||
|
## Select And Compose The Configuration You Need
|
||||||
|
|
||||||
|
Supply each backend, profile, prompt, schema, and collection source that the
|
||||||
|
application intends to use. Promptkit does not discover directories or install
|
||||||
|
provider definitions implicitly. An application may select an explicit
|
||||||
|
collection input with `CollectionInput`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
input, err := promptkit.CollectionInput(
|
||||||
|
"openrouter",
|
||||||
|
configkit.File("config/openrouter.yaml"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{
|
||||||
|
Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Collection resource IDs are local IDs, while their runtime identities are
|
||||||
|
qualified. For example, a profile with `id: gemini-flash-latest` in collection
|
||||||
|
`openrouter` is selected as `openrouter/gemini-flash-latest`. Stored backend,
|
||||||
|
prompt-default, and other resource references must use their exact intended IDs;
|
||||||
|
Promptkit does not rewrite references when a source is mounted. `extends`
|
||||||
|
selects a sibling ID in the same list, not a cross-collection reference.
|
||||||
|
Review collisions and all references when moving resources into collections.
|
||||||
|
|
||||||
|
The optional source-root `version` header now accepts `2` or `"2"`; headerless
|
||||||
|
sources remain valid. A collection's `release_version` remains an optional
|
||||||
|
label. Prompt-version labels have been removed: choose one definition per ID
|
||||||
|
and store any application-specific release label outside Promptkit. In a
|
||||||
|
standalone imported prompt item, root `version` is a source header, not a label.
|
||||||
|
|
||||||
|
For example, change a version-keyed definition:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
prompts:
|
||||||
|
report:
|
||||||
|
"2.0":
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: Review this report.
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
```
|
||||||
|
|
||||||
|
to one identified definition:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
prompts:
|
||||||
|
- id: report
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: Review this report.
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
```
|
||||||
|
|
||||||
|
The same conversion applies to backends and profiles. Sibling inheritance uses
|
||||||
|
identity references, and a later same-ID record replaces the whole lower record:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
profiles:
|
||||||
|
- id: base
|
||||||
|
endpoint: https://example.invalid/v1
|
||||||
|
model: example-model
|
||||||
|
- id: fast
|
||||||
|
extends: {id: base}
|
||||||
|
timeout_seconds: 30
|
||||||
|
```
|
||||||
|
|
||||||
|
To customize a lower resource's fields, add a distinct inherited variant as
|
||||||
|
above; when replacing the same ID, supply the complete intended record. Generic
|
||||||
|
append, replace, imports, and inheritance are described by the
|
||||||
|
[ConfigKit v1 configuration contract](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.0.0/docs/config.md).
|
||||||
|
Source-root controls are now `config:`, not `configkit:`. Imports are optional
|
||||||
|
unless the selected import declares `required: true`.
|
||||||
|
|
||||||
|
Update ConfigKit imports to `go.maximumdirect.net/configkit` at v1.0.0: its Go
|
||||||
|
types are incompatible with the old module path. Promptkit now requires Go
|
||||||
|
1.27.0 or newer. Source builders take ordered slices with explicit IDs:
|
||||||
|
|
||||||
|
```go
|
||||||
|
endpoint, model := "https://example.invalid/v1", "example-model"
|
||||||
|
base := "base"
|
||||||
|
source := promptkit.ProfileSource("application", []promptkit.ProfileConfig{
|
||||||
|
{ID: "base", Endpoint: &endpoint, Model: &model},
|
||||||
|
{ID: "fast", Extends: &base},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace former maps passed to `BackendSource` in the same way, setting each
|
||||||
|
`BackendConfig.ID`. Duplicates are retained for loading to reject. Resource
|
||||||
|
inspection paths now address list indexes, for example
|
||||||
|
`configkit.Keys("profiles").Index(0).Key("model")`. Configuration inspection
|
||||||
|
and diffs expose resolved values, recorded normalization and provenance.
|
||||||
|
`InspectProfile` additionally applies backend completion; request preparation
|
||||||
|
can apply further overrides, so the views are not promised to be identical.
|
||||||
|
|
||||||
|
Remove `PromptVersion` and `PromptIdentity.Version` uses and the corresponding
|
||||||
|
`prompt_version` JSON members. Prompt hashes are opaque equality values and may
|
||||||
|
change when the removed field no longer participates in hashing.
|
||||||
|
|
||||||
|
## Identity Mapping
|
||||||
|
|
||||||
|
The reviewed catalog identities have slash-free profile keys. Their documented
|
||||||
|
mapping is:
|
||||||
|
|
||||||
|
| Previous identity | Namespaced identity or action |
|
||||||
|
| --- | --- |
|
||||||
|
| Backend `openrouter` | `openrouter/default` |
|
||||||
|
| Backend `rakestrawhome` | `rakestrawhome/default` |
|
||||||
|
| Each OpenRouter catalog profile `p` | `openrouter/p`, retaining the exact local key `p` |
|
||||||
|
| Profile `rakestrawhome-gemma-4-31b` | `rakestrawhome/rakestrawhome-gemma-4-31b` |
|
||||||
|
| Built-in `promptkit-openrouter-standard` | No automatic replacement; optionally define caller profile `openrouter/standard` |
|
||||||
|
| Built-in `promptkit-openrouter-auto` | No automatic replacement; optionally define caller profile `openrouter/auto` |
|
||||||
|
|
||||||
|
These are conversion examples, not aliases or bundled resources. Mounting old
|
||||||
|
`/v3` assets does not update backend references, profile inheritance, prompt
|
||||||
|
defaults, or diagnostic defaults inside them. Edit those values to the exact
|
||||||
|
qualified IDs you chose.
|
||||||
|
|
||||||
|
For example, an optional caller-owned standard profile may extend a selected
|
||||||
|
Gemini profile in the same `openrouter` collection:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
collections:
|
||||||
|
openrouter:
|
||||||
|
profiles:
|
||||||
|
- id: standard
|
||||||
|
extends: {id: gemini-flash-latest}
|
||||||
|
```
|
||||||
|
|
||||||
|
An optional auto profile may instead select the exact backend and model values:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
collections:
|
||||||
|
openrouter:
|
||||||
|
profiles:
|
||||||
|
- id: auto
|
||||||
|
backend: openrouter/default
|
||||||
|
model: openrouter/auto
|
||||||
|
```
|
||||||
|
|
||||||
|
Neither optional profile name is a Promptkit contract. Keep provider model
|
||||||
|
strings and settings as selected by the application, including any meaningful
|
||||||
|
prefixes. If a backend declares diagnostic defaults, update them to exact
|
||||||
|
qualified IDs; an unselected diagnostic default is not automatically made
|
||||||
|
resolvable by migration.
|
||||||
|
|
||||||
|
## Prompt Selection API
|
||||||
|
|
||||||
|
Prompt selection is ID-only. Remove version selectors from calls to `Run`,
|
||||||
|
`Prepare`, `PrepareExecution`, `ValidateRequest`, `InspectRequirements`, and
|
||||||
|
`SmokeTest`. Remove the `PromptVersion` field from `RunRequest`,
|
||||||
|
`RequirementInspectionRequest`, and `SmokeTestRequest`; `InspectPrompt` now
|
||||||
|
has the form `InspectPrompt(ctx, promptID)`. For example, select the prompt
|
||||||
|
explicitly in the request rather than setting a version selector:
|
||||||
|
|
||||||
|
```go
|
||||||
|
request := promptkit.RunRequest{
|
||||||
|
PromptID: "support/report",
|
||||||
|
ProfileID: "support/standard",
|
||||||
|
}
|
||||||
|
_, err = engine.Run(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the former format contained multiple versions under one ID, choose the one
|
||||||
|
definition the application should install. Promptkit no longer retains or
|
||||||
|
returns its former descriptive label.
|
||||||
|
|
||||||
|
The public inventory APIs can help an operator inspect a captured effective
|
||||||
|
configuration: `ListProfiles`, `ListPrompts`, and `ListBackends` return exact
|
||||||
|
identities, while `ListNamespaces` and `ListCollections` expose resource-bearing
|
||||||
|
namespaces and effective collection declarations. Inventory does not select
|
||||||
|
which configuration version the application should deploy.
|
||||||
|
|
||||||
|
## Deployment Selection Remains With The Application
|
||||||
|
|
||||||
|
Applications decide which checked-in, embedded, operator-supplied, or
|
||||||
|
programmatic inputs to compose for each deployment. They also decide when to
|
||||||
|
construct a replacement engine after changing those sources. Promptkit does
|
||||||
|
not fetch catalog updates, select provider availability, validate upstream
|
||||||
|
catalog release status, or automatically migrate arbitrary consumer files.
|
||||||
|
The maintained [offline collection example](../../examples/go-library/prepare/main.go)
|
||||||
|
shows explicit source selection, local imports, layering, a scoped view, and
|
||||||
|
operator inventory without a provider call.
|
||||||
@@ -12,63 +12,356 @@ file contracts.
|
|||||||
Import the package as:
|
Import the package as:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "gitea.maximumdirect.net/eric/promptkit"
|
import "go.maximumdirect.net/promptkit"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For migration from the former module path, see
|
||||||
|
[Update Go Imports](configuration-migration.md#update-go-imports).
|
||||||
|
|
||||||
|
Configuration APIs use `go.maximumdirect.net/configkit` v1.1.0. Its types are
|
||||||
|
not interchangeable with the old ConfigKit module path.
|
||||||
|
|
||||||
|
The maintained [offline preparation example](../../examples/go-library/prepare/main.go)
|
||||||
|
demonstrates explicit collection composition and inventory. Applications
|
||||||
|
adapting older provider or versioned-prompt configuration can use the
|
||||||
|
[configuration migration guide](configuration-migration.md) for the required
|
||||||
|
caller-owned choices.
|
||||||
|
|
||||||
The following Go fragments are illustrative and omit surrounding package,
|
The following Go fragments are illustrative and omit surrounding package,
|
||||||
import, and error-handling code. Use the maintained examples for complete
|
import, and error-handling code. Use the maintained examples for complete
|
||||||
programs.
|
programs.
|
||||||
|
|
||||||
|
## Go Compatibility
|
||||||
|
|
||||||
|
The `go` directive in the selected release's [module metadata](../../go.mod)
|
||||||
|
declares its minimum supported consumer Go version, currently Go 1.27.0.
|
||||||
|
Maintainers build and
|
||||||
|
validate releases with the locally selected Go patch release; the minimum
|
||||||
|
does not simply track that patch version.
|
||||||
|
|
||||||
|
Successful release validation on the selected patch is sufficient to presume
|
||||||
|
support across its Go series, subject to the declared minimum and concrete
|
||||||
|
evidence of incompatibility. Specific compatibility concerns are investigated
|
||||||
|
as they arise. Minor releases may raise the minimum; patch releases preserve
|
||||||
|
it. Release notes explain increases and any required consumer action. See the
|
||||||
|
[release policy](../policy/release.md#consumer-minimum-and-release-toolchain).
|
||||||
|
|
||||||
## Construct An Engine
|
## Construct An Engine
|
||||||
|
|
||||||
Create an engine with
|
Create an engine with [`NewEngine`](../../engine.go). Select each directory
|
||||||
[`NewEngine`](../../engine.go). A directory-backed setup supplies a prompt
|
explicitly and place its inputs in an ordered ConfigKit underlay. A group
|
||||||
directory and may supply profile and schema directories:
|
strictly composes its files; later groups can overlay earlier ones:
|
||||||
|
|
||||||
|
The bundled OpenRouter backend and profiles are installed in a lower underlay
|
||||||
|
by default. Select a profile explicitly, for example:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
input := configkit.Input{Source: configkit.Mapping("application", map[string]any{
|
||||||
|
"prompts": []any{map[string]any{
|
||||||
|
"id": "my-prompt",
|
||||||
|
"messages": []any{map[string]any{"role": "user", "content": "Hello"}},
|
||||||
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
|
}},
|
||||||
|
})}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}})
|
||||||
|
if err != nil { return err }
|
||||||
|
result, err := engine.Run(ctx, promptkit.RunRequest{PromptID: "my-prompt", ProfileID: "gpt-luna-latest"})
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend looks up
|
||||||
|
`OPENROUTER_API_KEY` when available; credentials are not required at engine
|
||||||
|
construction. For the complete inventory and diagnostic defaults see the
|
||||||
|
[format reference](../formats.md#bundled-openrouter-configuration). Use
|
||||||
|
`promptkit.WithBundledOpenRouter(false)` for application-only inventories.
|
||||||
|
Caller records replace matching bundled records in full; use `extends` with a
|
||||||
|
distinct ID to inherit. Replacing `auto` also changes its cost-tier children's
|
||||||
|
inherited target, while their plugins remain. External collections remain
|
||||||
|
supported.
|
||||||
|
|
||||||
|
```go
|
||||||
|
promptInputs, err := promptkit.DirectoryInputs("prompts")
|
||||||
|
if err != nil { return err }
|
||||||
|
profileInputs, err := promptkit.DirectoryInputs("profiles")
|
||||||
|
if err != nil { return err }
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{
|
engine, err := promptkit.NewEngine(promptkit.Config{
|
||||||
PromptDir: "prompts",
|
Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{
|
||||||
ProfileDir: "profiles",
|
{Inputs: promptInputs},
|
||||||
SchemaDir: "schemas",
|
{Inputs: profileInputs},
|
||||||
|
}},
|
||||||
|
SchemaDir: "schemas",
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
Options support single-file or `fs.FS` sources, in-memory profiles,
|
For a single highest-priority document, set `Sources.Main` to a
|
||||||
engine-scoped backends, and injected artifact or model clients. Consult the
|
`*configkit.Input`; omit it for ordinary layered construction. A single file
|
||||||
[constructor and option GoDoc](../../engine.go) for composition, precedence,
|
can use `configkit.File` directly. `ProfileSource`, `BackendSource`, and raw
|
||||||
validation, and default transport behavior. Source discovery, format
|
`configkit.Mapping` supply programmatic definitions. Schema and runtime
|
||||||
validation, and profile precedence are defined by the
|
collaborator options remain available. Construction validates the final
|
||||||
[framework format reference](../formats.md).
|
effective configuration and captures referenced prompt content; construct a
|
||||||
|
new engine to observe configuration edits. See the [constructor GoDoc](../../engine.go)
|
||||||
|
for the exact public contract and the [framework format reference](../formats.md)
|
||||||
|
for definition fields.
|
||||||
|
|
||||||
|
### Use Scoped Engine Views
|
||||||
|
|
||||||
|
Create a view when an application wants short request IDs to resolve within a
|
||||||
|
selected collection:
|
||||||
|
|
||||||
|
```go
|
||||||
|
scoped, err := engine.WithNamespaces(promptkit.Namespaces{
|
||||||
|
Prompt: "support",
|
||||||
|
Profile: "support",
|
||||||
|
})
|
||||||
|
if err != nil { return err }
|
||||||
|
|
||||||
|
prepared, err := scoped.Prepare(ctx, promptkit.RunRequest{
|
||||||
|
PromptID: "answer",
|
||||||
|
ProfileID: "concise",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Execution, preparation, prompt/profile inspection, requirement inspection, and
|
||||||
|
smoke diagnostics qualify slashless caller selections with their respective
|
||||||
|
namespaces. Already-qualified IDs pass through unchanged; configured defaults
|
||||||
|
and references are not rewritten. Each call replaces the view's scope defaults,
|
||||||
|
including clearing a scope with an empty field, without changing the receiver
|
||||||
|
or rebuilding its captured configuration and execution collaborators.
|
||||||
|
Namespace syntax is checked when creating the view; resource existence is
|
||||||
|
checked when selected. Configuration inspection remains global. See
|
||||||
|
[`WithNamespaces`](../../namespaces.go) for the exact public contract.
|
||||||
|
|
||||||
## Supply Embedded Application Defaults
|
## Supply Embedded Application Defaults
|
||||||
|
|
||||||
Use `WithFallbackProfileFS` when an application packages profile definitions
|
Use `FilesystemInputs` for embedded defaults and `DirectoryInputs` for an
|
||||||
that should apply unless an operator provides an ordinary configured profile
|
explicitly chosen operator directory. A later programmatic group can override
|
||||||
with the same ID. For example, an application can embed its defaults while
|
both. No directory name has built-in precedence:
|
||||||
continuing to use `ProfileDir` for operator overrides:
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
//go:embed profiles/*.yaml
|
defaults, err := promptkit.FilesystemInputs("embedded defaults", applicationFS, "config")
|
||||||
var applicationProfiles embed.FS
|
if err != nil { return err }
|
||||||
|
operator, err := promptkit.DirectoryInputs(operatorDirectory)
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{
|
if err != nil { return err }
|
||||||
PromptDir: "prompts",
|
profile := promptkit.ProfileSource("application choices", []promptkit.ProfileConfig{
|
||||||
ProfileDir: operatorProfileDir,
|
{ID: "summary", Endpoint: &selectedEndpoint, Model: &selectedModel},
|
||||||
},
|
})
|
||||||
promptkit.WithFallbackProfileFS(applicationProfiles, "profiles"),
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
)
|
Underlays: []configkit.Underlay{
|
||||||
|
{Inputs: defaults},
|
||||||
|
{Inputs: operator},
|
||||||
|
{Inputs: []configkit.Input{{Source: profile}}},
|
||||||
|
},
|
||||||
|
}})
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep application-owned profile IDs and definitions in the embedded source.
|
Each later same-ID resource replaces the complete earlier record; use a distinct
|
||||||
Use the ordinary configured profile source for operator overrides. Leave
|
inherited sibling for field-level customization. Only add the operator group
|
||||||
`operatorProfileDir` empty when the operator did not configure an override
|
when the application selects that directory; an
|
||||||
directory; a non-empty path names an authoritative higher-precedence source,
|
explicitly selected missing or unreadable root is an error. Each helper finds
|
||||||
so an unavailable or unreadable directory is an error rather than a reason to
|
`.yaml` and `.yml` files recursively and disables imports by default. The
|
||||||
fall back. The
|
caller may enable imports on individual inputs. A nil `Sources.Main` is an
|
||||||
[framework format reference](../formats.md#source-and-profile-precedence)
|
empty main source, not another directory lookup.
|
||||||
owns the exact profile format and lookup order; the
|
|
||||||
[`WithFallbackProfileFS` GoDoc](../../engine.go) owns its option contract and
|
For a programmatic document with explicit null or a prompt definition, use a
|
||||||
validation rules.
|
labeled mapping as an input. A mapping owns a snapshot of supported values:
|
||||||
|
|
||||||
|
```go
|
||||||
|
document := configkit.Mapping("application configuration", map[string]any{
|
||||||
|
"profiles": []any{
|
||||||
|
map[string]any{"id": "local-summary",
|
||||||
|
"backend": "local", "model": "example-model",
|
||||||
|
"temperature": nil, // Suppress an inherited provider control.
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
input := configkit.Input{Source: document}
|
||||||
|
// Add input to a later underlay, or set Sources.Main = &input.
|
||||||
|
```
|
||||||
|
|
||||||
|
`ProfileSource` and `BackendSource` are smaller typed conveniences when null
|
||||||
|
is not needed. Backend and profile definitions are supplied by the caller;
|
||||||
|
there is no registration-only backend namespace.
|
||||||
|
|
||||||
|
When configuration or message files change, build and validate a new engine,
|
||||||
|
then switch the application to that engine for subsequent work. Existing
|
||||||
|
engines and prepared handles retain their captured definitions; schema and
|
||||||
|
request artifact resources still follow their runtime lifecycle. Inspection
|
||||||
|
does not watch or reload sources automatically.
|
||||||
|
|
||||||
|
## Discover Available Resources And Collections
|
||||||
|
|
||||||
|
Use [`Engine.ListProfiles`](../../listing.go),
|
||||||
|
[`Engine.ListPrompts`](../../listing.go), and
|
||||||
|
[`Engine.ListBackends`](../../listing.go) to discover exact identities from the
|
||||||
|
engine's final captured configuration. A scoped view filters by its respective
|
||||||
|
namespace by default; pass `NamespaceNamed`, `NamespaceUnqualified`, or
|
||||||
|
`NamespaceAll` to override that listing filter without changing execution
|
||||||
|
scope. Returned IDs remain full identities and can be used for exact selection:
|
||||||
|
|
||||||
|
```go
|
||||||
|
profiles, err := engine.ListProfiles(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(profiles) > 0 {
|
||||||
|
_, err := engine.InspectProfile(ctx, profiles[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prompts, err := engine.ListPrompts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(prompts) > 0 {
|
||||||
|
selected := prompts[0]
|
||||||
|
inspection, err := engine.InspectPrompt(ctx, selected.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = inspection
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use [`Engine.ListNamespaces`](../../listing.go) to discover resource-bearing
|
||||||
|
namespaces across the entire captured configuration, independent of view
|
||||||
|
scopes. [`Engine.ListCollections`](../../listing.go) reports effective
|
||||||
|
collection declarations, including metadata-only entries; it does not infer
|
||||||
|
collection metadata from resource membership or source history.
|
||||||
|
|
||||||
|
These lists describe effective membership, not ConfigKit contribution history,
|
||||||
|
and do not promise that a profile is ready to execute. Use inspection for
|
||||||
|
details and preparation to resolve request resources. The linked method GoDoc
|
||||||
|
owns ordering, ownership, concurrency, and error behavior; identifiers may be
|
||||||
|
sensitive application data.
|
||||||
|
|
||||||
|
## Inspect The Captured Configuration
|
||||||
|
|
||||||
|
Use [`Engine.InspectConfiguration`](../../configuration_inspection.go) for an
|
||||||
|
operator-selected report of resolved configuration, source descriptions,
|
||||||
|
diagnostics, and path history. Configuration inspection uses list indexes in
|
||||||
|
the collection's nested path rather than qualified runtime IDs. For a profile
|
||||||
|
at index `i` in collection `team`, construct the path with `configkit.Keys`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
view, err := engine.InspectConfiguration()
|
||||||
|
if err != nil { return err }
|
||||||
|
path := configkit.Keys("collections", "team", "profiles").Index(i).Key("model")
|
||||||
|
explanation, found := view.Explain(path)
|
||||||
|
if found && explanation.Effective.Redacted {
|
||||||
|
// Apply the application's disclosure policy before sharing the report.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`WriteEffective` and `WriteExplanation` produce ConfigKit-redacted reports;
|
||||||
|
the application decides whether those reports may be disclosed. Credential-
|
||||||
|
shaped provider keys are redacted, but arbitrary prompt and provider content
|
||||||
|
is not necessarily safe to share. Inspection uses the engine's captured result
|
||||||
|
and does not reopen sources or resolve credentials. It retains effective values,
|
||||||
|
including recorded field normalization and queue defaulting, alongside source
|
||||||
|
provenance.
|
||||||
|
`InspectProfile` instead shows backend-completed execution settings for one profile, and `Prepare` or
|
||||||
|
`PreparedExecution.Details` shows request-specific rendered messages, inputs,
|
||||||
|
and validation resources. None of those views is a substitute for the others.
|
||||||
|
|
||||||
|
To review changes between two independently captured configurations, compare
|
||||||
|
their inspection views:
|
||||||
|
|
||||||
|
```go
|
||||||
|
earlier, err := earlierEngine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
later, err := candidateEngine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
differences, err := earlier.Diff(later)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, difference := range differences {
|
||||||
|
if difference.HiddenChanged {
|
||||||
|
// Report a protected change without inferring its value.
|
||||||
|
}
|
||||||
|
if difference.Left.Redacted || difference.Right.Redacted {
|
||||||
|
// Apply the application's disclosure policy before presenting snapshots.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [`ConfigurationInspection.Diff` GoDoc](../../configuration_inspection.go)
|
||||||
|
and ConfigKit's [v1.1.0 effective-configuration diff guidance](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.1.0/docs/config.md#effective-configuration-diffs)
|
||||||
|
for the exact comparison contract. The operation compares only frozen effective
|
||||||
|
configuration values: it does not reread external files or compare their
|
||||||
|
contents, resolved credential contents, later execution preparation, or
|
||||||
|
request-specific overrides. Inputs normalizing to the same effective value do
|
||||||
|
not create a value diff; lists are compared atomically. An empty diff is not
|
||||||
|
proof of equivalent execution.
|
||||||
|
Records can contain ordinary application data, so use the application's
|
||||||
|
disclosure policy when presenting them.
|
||||||
|
|
||||||
|
## Run Explicit Smoke Diagnostics
|
||||||
|
|
||||||
|
[`Engine.SmokeTest`](../../smoke_diagnostics.go) runs one explicit diagnostic
|
||||||
|
using a target resolved from the engine's captured configuration. It does not
|
||||||
|
execute the selected application prompt or its inputs, session, artifacts, or
|
||||||
|
output schema. Unlike the offline examples and default test suite, calling it
|
||||||
|
can contact the selected provider and use configured credentials. It is a
|
||||||
|
consumer operation, not an automatic readiness verdict or a release check.
|
||||||
|
|
||||||
|
For example, a caller can request the built-in text inference check and treat
|
||||||
|
its optional fact as an observation rather than assuming that a nil Go error
|
||||||
|
means success:
|
||||||
|
|
||||||
|
```go
|
||||||
|
report, err := engine.SmokeTest(ctx, promptkit.SmokeTestRequest{
|
||||||
|
Kind: promptkit.SmokeTestTextInference,
|
||||||
|
ProfileID: profileID,
|
||||||
|
APIKey: apiKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if report != nil {
|
||||||
|
retainDisclosureSafeSummary(report)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if report != nil {
|
||||||
|
matched := report.Facts.ExpectedTextMatched
|
||||||
|
switch {
|
||||||
|
case matched == nil:
|
||||||
|
// The comparison was not evaluated or is unknown/inapplicable.
|
||||||
|
case *matched:
|
||||||
|
// The private expected-text comparison matched.
|
||||||
|
default:
|
||||||
|
// It did not match; the operation may still have concluded normally.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A nil Go error means the diagnostic concluded, not that its finding was
|
||||||
|
positive. Fact pointers are tri-state: nil means unknown, not evaluated, or
|
||||||
|
inapplicable. Reports contain factual observations and effective settings, not
|
||||||
|
generated text or provider bodies; identifiers and settings can still be
|
||||||
|
sensitive. The
|
||||||
|
[`SmokeTest` GoDoc](../../smoke_diagnostics.go) owns selection precedence,
|
||||||
|
exact fields, timeout and retry behavior, supported transports, and disclosure
|
||||||
|
details.
|
||||||
|
|
||||||
|
Caller cancellation can return both an error and a partial report. Preserve the
|
||||||
|
context error identity and inspect any available observations before returning:
|
||||||
|
|
||||||
|
```go
|
||||||
|
report, err := engine.SmokeTest(ctx, request)
|
||||||
|
if report != nil {
|
||||||
|
retainDisclosureSafeSummary(report)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
// The report may describe work completed before interruption.
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Inspect A Prompt Before Preparation
|
## Inspect A Prompt Before Preparation
|
||||||
|
|
||||||
@@ -77,7 +370,7 @@ declared inputs and output workflow without creating placeholder inputs or
|
|||||||
resolving a profile:
|
resolving a profile:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
inspection, err := engine.InspectPrompt(ctx, "meeting.summary", "")
|
inspection, err := engine.InspectPrompt(ctx, "meeting.summary")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -94,6 +387,85 @@ rendered messages, and use prepared execution when that work must remain tied
|
|||||||
to later execution. The method's [GoDoc](../../engine.go) owns exact fields,
|
to later execution. The method's [GoDoc](../../engine.go) owns exact fields,
|
||||||
hash, ownership, and error semantics.
|
hash, ownership, and error semantics.
|
||||||
|
|
||||||
|
## Inspect Requirements Before Preparation
|
||||||
|
|
||||||
|
Use [`Engine.InspectRequirements`](../../requirements.go) when the application
|
||||||
|
needs the prompt's recognizable input and caller-variable dependencies for a
|
||||||
|
particular context before it has input artifacts. Unlike `InspectPrompt`, it
|
||||||
|
also walks captured template syntax and selects a profile identity; unlike
|
||||||
|
`Prepare`, it does not load artifacts or render messages.
|
||||||
|
|
||||||
|
```go
|
||||||
|
requirements, err := engine.InspectRequirements(ctx, promptkit.RequirementInspectionRequest{
|
||||||
|
PromptID: "meeting.summary",
|
||||||
|
Vars: map[string]string{"audience": "project leads"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, input := range requirements.Inputs {
|
||||||
|
if input.Kind == promptkit.RequirementRequired {
|
||||||
|
// Plan to bind this input before preparation.
|
||||||
|
}
|
||||||
|
if input.Kind == promptkit.RequirementOptional && input.Explanation != "" {
|
||||||
|
// A recognizable reference may occur only on a known skipped path.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, variable := range requirements.Variables {
|
||||||
|
if variable.Kind == promptkit.RequirementRequired && !variable.Supplied {
|
||||||
|
// The key is required even though this inspection did not receive it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !requirements.Complete {
|
||||||
|
// Preserve the partial inventory and account for its discovery issues.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional entries can include recognizable inputs and variables found only in
|
||||||
|
inactive branches. A missing required variable is reported separately from
|
||||||
|
discovery completeness: missing a value used only for direct text substitution
|
||||||
|
does not by itself make the inventory incomplete. Conversely, an unresolved
|
||||||
|
dependency-bearing branch can make discovery incomplete while retaining known
|
||||||
|
conditional entries. Do not treat an empty partial inventory as proof that no
|
||||||
|
input bindings are needed. See the method's [GoDoc](../../requirements.go) for
|
||||||
|
the exact classifications, supported syntax, ownership, and error contract.
|
||||||
|
|
||||||
|
## Validate A Concrete Request
|
||||||
|
|
||||||
|
Use [`Engine.ValidateRequest`](../../engine.go) when the application has a
|
||||||
|
complete `RunRequest` and needs to know whether that concrete invocation passes
|
||||||
|
preparation:
|
||||||
|
|
||||||
|
```go
|
||||||
|
request := promptkit.RunRequest{
|
||||||
|
PromptID: "meeting.summary",
|
||||||
|
Vars: map[string]string{"audience": "project leads"},
|
||||||
|
Inputs: map[string]promptkit.ArtifactRef{
|
||||||
|
"note": promptkit.Inline("Synthetic meeting notes"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := engine.ValidateRequest(ctx, request); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Unlike `InspectRequirements`, this checks supplied values, the selected template
|
||||||
|
branches, and request bindings through the existing preparation path. It checks
|
||||||
|
only this invocation, does not aggregate errors, and loads every supplied input
|
||||||
|
according to ordinary preparation behavior, even if a branch does not use it.
|
||||||
|
It makes no model call and returns no prepared snapshot, but artifact and schema
|
||||||
|
resource I/O and its time and memory costs remain. Success does not certify
|
||||||
|
provider acceptance or later execution success. See the method's GoDoc for its
|
||||||
|
exact error and context behavior.
|
||||||
|
|
||||||
|
Scriptorium can map the returned error to its own diagnostics and exit status.
|
||||||
|
Its `validate` and `run` commands should construct equivalent requests for
|
||||||
|
equivalent caller inputs so their preparation behavior agrees. Separate
|
||||||
|
subprocess calls prepare independently; an opaque `PrepareExecution` handle
|
||||||
|
cannot be transferred from `validate` to `run`. Use `PrepareExecution` when one
|
||||||
|
process and engine must execute the exact prepared snapshot.
|
||||||
|
|
||||||
## Prepare Without Model Execution
|
## Prepare Without Model Execution
|
||||||
|
|
||||||
[`Engine.Prepare`](../../engine.go) resolves the selected prompt and profile,
|
[`Engine.Prepare`](../../engine.go) resolves the selected prompt and profile,
|
||||||
@@ -114,8 +486,11 @@ prepared, err := engine.Prepare(ctx, promptkit.RunRequest{
|
|||||||
The maintained
|
The maintained
|
||||||
[offline preparation example](../../examples/go-library/prepare/main.go)
|
[offline preparation example](../../examples/go-library/prepare/main.go)
|
||||||
shows a complete runnable setup with a prompt file, in-memory profile, and
|
shows a complete runnable setup with a prompt file, in-memory profile, and
|
||||||
inline input. Exact request requirements and prepared-result fields belong to
|
inline input. It demonstrates validation and preparation with the same request
|
||||||
the [`RunRequest` and `PreparedRun` GoDoc](../../types.go).
|
to make the two operations concrete; this performs preparation twice and is
|
||||||
|
illustrative, not a requirement to validate before `Prepare` or `Run`. Exact
|
||||||
|
request requirements and prepared-result fields belong to the
|
||||||
|
[`RunRequest` and `PreparedRun` GoDoc](../../types.go).
|
||||||
|
|
||||||
## Prepare Now And Execute The Same Snapshot Later
|
## Prepare Now And Execute The Same Snapshot Later
|
||||||
|
|
||||||
@@ -268,8 +643,8 @@ result, err := engine.Run(ctx, request)
|
|||||||
|
|
||||||
These messages are already rendered: Promptkit does not template or resolve
|
These messages are already rendered: Promptkit does not template or resolve
|
||||||
files in them, and they can contain sensitive model output or application
|
files in them, and they can contain sensitive model output or application
|
||||||
feedback. Promptkit remains stateless; every `Run` call re-resolves its current
|
feedback. Every `Run` call uses the engine's captured configuration; the
|
||||||
sources and the application owns any semantic retry budget. When a
|
application owns any semantic retry budget. When a
|
||||||
pre-execution equality check is required, use `PrepareExecution` and compare
|
pre-execution equality check is required, use `PrepareExecution` and compare
|
||||||
its opaque rendered-prompt hash before invoking `RunPrepared`.
|
its opaque rendered-prompt hash before invoking `RunPrepared`.
|
||||||
|
|
||||||
@@ -282,84 +657,48 @@ replace execution settings or the complete output contract.
|
|||||||
The [public value GoDoc](../../types.go) defines nil, empty, zero, replacement,
|
The [public value GoDoc](../../types.go) defines nil, empty, zero, replacement,
|
||||||
copy, and credential behavior. The
|
copy, and credential behavior. The
|
||||||
[framework format reference](../formats.md) defines how those request values
|
[framework format reference](../formats.md) defines how those request values
|
||||||
interact with prompt definitions, file-backed and application fallback
|
interact with prompt definitions, layered profiles, built-ins, schemas, and
|
||||||
profiles, built-ins, schemas, and framework defaults.
|
framework defaults.
|
||||||
|
|
||||||
For programmatic profiles,
|
For programmatic profiles, use [`ProfileSource`](../../profiles.go) with identified
|
||||||
[`OpenAICompatibleProfile`](../../profiles.go) converts ordinary
|
`ProfileConfig` values. Nil fields are omitted; pointers preserve explicit
|
||||||
OpenAI-compatible settings into a value accepted by `WithProfiles`.
|
zero, false, and blank values. Raw `configkit.Mapping` expresses explicit null.
|
||||||
|
|
||||||
|
### Migrate From The Previous Configuration API
|
||||||
|
|
||||||
|
Replace `Config.PromptDir` and `Config.ProfileDir` with selected ConfigKit
|
||||||
|
inputs in `Config.Sources`. The removed `WithPromptFS`, `WithPromptFile`,
|
||||||
|
`WithProfileFS`, `WithProfileFile`, `WithFallbackProfileFS`, `WithProfiles`,
|
||||||
|
and `WithBackend` options have no compatibility wrappers. Use
|
||||||
|
`DirectoryInputs` or `FilesystemInputs` for a strict-composition file group,
|
||||||
|
ConfigKit file inputs for individual files, and `ProfileSource`,
|
||||||
|
`BackendSource`, or `configkit.Mapping` for programmatic values. Backend IDs
|
||||||
|
are fields in configuration, not registrations. Choose caller underlays in
|
||||||
|
fallback-to-operator-to-programmatic order; use `Sources.Main` for one final
|
||||||
|
highest-priority input.
|
||||||
|
|
||||||
|
Move resource IDs into identified `backends`, `profiles`, and `prompts` lists;
|
||||||
|
each prompt ID identifies one installed definition without a version label.
|
||||||
|
Convert `base_profile` to ConfigKit `extends: {id: parent}`. For the current
|
||||||
|
collection identity mapping, ID-only prompt selection, and required
|
||||||
|
caller-owned choices, see the
|
||||||
|
[configuration migration guide](configuration-migration.md). Exact current
|
||||||
|
format, reference, inheritance, and presence semantics belong to the
|
||||||
|
[framework format reference](../formats.md). Prompt text and static templates
|
||||||
|
are captured at construction, not automatically reloaded between runs. Replace
|
||||||
|
the engine to pick up changed definitions or message files.
|
||||||
|
|
||||||
### Choose A Profile Ownership Model
|
### Choose A Profile Ownership Model
|
||||||
|
|
||||||
Choose the profile source that matches the application's continuity needs:
|
Supply backend and profile definitions through the configuration sources your
|
||||||
|
application selects, alongside the default-on bundled OpenRouter inventory.
|
||||||
- Select `promptkit-openrouter-standard` or `promptkit-openrouter-auto` when
|
Conventional backend ID constants are names only. Define or replace the model,
|
||||||
the maintained literal name is the desired consumer contract. Their current
|
endpoint, credentials lookup, and settings that your application
|
||||||
selections are in the [format reference](../formats.md#built-in-profile-catalog).
|
intends to support. A profile may inherit a sibling profile by its local key in
|
||||||
- Select an external catalog profile directly when the application accepts that
|
the same root or collection list; its backend reference uses the exact
|
||||||
profile's catalog volatility. The selected external inventories are linked
|
configured identity. Caller-selected source layers determine the final values. The
|
||||||
from the [format reference](../formats.md#external-profile-inventories).
|
[format reference](../formats.md) describes the definition fields and
|
||||||
- Define a complete application-owned profile with an explicit backend or
|
composition rules.
|
||||||
endpoint, model, and settings when the application owns those choices.
|
|
||||||
|
|
||||||
An application profile that inherits an external base follows later changes to
|
|
||||||
that base and fails if the base disappears; it is not a frozen copy. Promptkit
|
|
||||||
tests the catalog versions selected in its module graph. Independently advancing
|
|
||||||
a catalog dependency can remove an inherited base and produce an untested
|
|
||||||
combination. Pinning the resolved module graph preserves the embedded data for
|
|
||||||
a build, but cannot preserve provider availability. Running binaries neither
|
|
||||||
download catalog updates nor automatically replace withdrawn models. See the
|
|
||||||
[release policy](../policy/release.md#catalog-profile-compatibility) for the exact
|
|
||||||
cross-version guarantees.
|
|
||||||
|
|
||||||
### Alias A Built-In Profile
|
|
||||||
|
|
||||||
Give an application-owned profile ID a built-in base when prompts should select
|
|
||||||
the application ID while inheriting the built-in target. The child can override
|
|
||||||
only the setting it owns:
|
|
||||||
|
|
||||||
```go
|
|
||||||
promptkit.WithProfiles(promptkit.Profile{
|
|
||||||
ID: "weather-light",
|
|
||||||
BaseProfileID: "promptkit-openrouter-standard",
|
|
||||||
ReasoningEffort: "high",
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
Select `weather-light` in a prompt or `RunRequest.ProfileID`; it remains the
|
|
||||||
reported selected profile. See the [profile inheritance format
|
|
||||||
reference](../formats.md#profile-inheritance) and the
|
|
||||||
[`Profile` GoDoc](../../types.go) for exact lookup, merging, and validation
|
|
||||||
behavior. If the base is an external profile, apply the volatility guidance
|
|
||||||
above when deciding whether to use an application-owned complete definition
|
|
||||||
instead.
|
|
||||||
|
|
||||||
### Use The Rakestrawhome Built-In Profile
|
|
||||||
|
|
||||||
Set `RAKESTRAWHOME_INFERENCE_API_KEY` in the application environment, then
|
|
||||||
select `rakestrawhome-gemma-4-31b` as an ordinary profile ID. For example, a
|
|
||||||
prepared result identifies the selected built-in through
|
|
||||||
`BackendRakestrawHome`:
|
|
||||||
|
|
||||||
```go
|
|
||||||
prepared, err := engine.Prepare(ctx, promptkit.RunRequest{
|
|
||||||
PromptID: "meeting.summary",
|
|
||||||
ProfileID: "rakestrawhome-gemma-4-31b",
|
|
||||||
Inputs: inputs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if prepared.SelectedBackendID != promptkit.BackendRakestrawHome {
|
|
||||||
return fmt.Errorf("unexpected backend %q", prepared.SelectedBackendID)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not register `rakestrawhome` manually. When adopting this built-in, remove
|
|
||||||
an existing `WithBackend` registration with that exact ID; retaining it causes
|
|
||||||
the intentional duplicate-ID configuration error. Direct request credentials
|
|
||||||
and runtime endpoint overrides remain supported under their ordinary GoDoc and
|
|
||||||
format contracts.
|
|
||||||
|
|
||||||
### Inspect A Profile Before Prompt Work
|
### Inspect A Profile Before Prompt Work
|
||||||
|
|
||||||
@@ -424,19 +763,15 @@ Choose the smallest configuration that fits how the endpoint will be reused.
|
|||||||
|
|
||||||
#### Use An Endpoint-Only Profile
|
#### Use An Endpoint-Only Profile
|
||||||
|
|
||||||
Put the endpoint directly on an in-memory profile when only that profile needs
|
Put the endpoint directly on a programmatic profile when only that profile needs
|
||||||
it and shared backend identity or capacity policy is unnecessary:
|
it and shared backend identity or capacity policy is unnecessary:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{
|
endpoint, model := "http://localhost:8000/v1", "example-model"
|
||||||
PromptDir: "prompts",
|
profile := promptkit.ProfileSource("local profile", []promptkit.ProfileConfig{
|
||||||
},
|
{ID: "local-summary", Endpoint: &endpoint, Model: &model},
|
||||||
promptkit.WithProfiles(promptkit.Profile{
|
})
|
||||||
ID: "local-summary",
|
// Add configkit.Input{Source: profile} to a caller underlay.
|
||||||
Endpoint: "http://localhost:8000/v1",
|
|
||||||
Model: "example-model",
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Endpoint-only profiles have an empty backend ID and remain unrestricted by
|
Endpoint-only profiles have an empty backend ID and remain unrestricted by
|
||||||
@@ -448,59 +783,47 @@ Use `LocalBackend` when profiles should share the conventional `local`
|
|||||||
identity, endpoint, and concurrency limit:
|
identity, endpoint, and concurrency limit:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{
|
backendID, model := promptkit.BackendLocal, "example-model"
|
||||||
PromptDir: "prompts",
|
localBackend := promptkit.LocalBackend("http://localhost:8000/v1", 2)
|
||||||
},
|
localBackend.ID = backendID
|
||||||
promptkit.WithBackend(
|
backend := promptkit.BackendSource("local backend", []promptkit.BackendConfig{
|
||||||
promptkit.LocalBackend("http://localhost:8000/v1", 2),
|
localBackend,
|
||||||
),
|
})
|
||||||
promptkit.WithProfiles(promptkit.Profile{
|
profile := promptkit.ProfileSource("local profile", []promptkit.ProfileConfig{
|
||||||
ID: "local-summary",
|
{ID: "local-summary", Backend: &backendID, Model: &model},
|
||||||
BackendID: promptkit.BackendLocal,
|
})
|
||||||
Model: "example-model",
|
// Add both sources as inputs in a caller underlay.
|
||||||
}),
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The helper is explicit: it does not pre-register a backend or read environment
|
The helper does not choose an ID or read environment variables. A positive
|
||||||
variables. Supplying a positive limit leaves queue capacity omitted, so normal
|
concurrency limit with omitted queue capacity selects the default waiting
|
||||||
backend registration selects the existing default waiting capacity of 1024.
|
capacity of 1024.
|
||||||
The returned value still enters the engine through `WithBackend`.
|
|
||||||
|
|
||||||
#### Configure A Complete Backend
|
#### Configure A Complete Backend
|
||||||
|
|
||||||
Use a keyed `Backend` value for authentication, extra request parameters, an
|
Use an identified `BackendConfig` value for authentication, extra request parameters, an
|
||||||
explicit queue capacity, a custom ID, or multiple local endpoints:
|
explicit queue capacity, a custom ID, or multiple local endpoints:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
noWaiting := 0
|
endpoint, keyName, limit, noWaiting := "http://gpu-host:8000/v1", "LOCAL_GPU_API_KEY", 2, 0
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{
|
backend := promptkit.BackendSource("GPU backend", []promptkit.BackendConfig{
|
||||||
PromptDir: "prompts",
|
{
|
||||||
},
|
ID: "local-gpu",
|
||||||
promptkit.WithBackend(promptkit.Backend{
|
Endpoint: &endpoint, APIKeyEnv: &keyName,
|
||||||
ID: "local-gpu",
|
ConcurrencyLimit: &limit, QueueCapacity: &noWaiting,
|
||||||
Endpoint: "http://gpu-host:8000/v1",
|
ExtraParams: map[string]any{"provider_option": "enabled"},
|
||||||
APIKeyEnv: "LOCAL_GPU_API_KEY",
|
},
|
||||||
ExtraParams: map[string]any{"provider_option": "enabled"},
|
})
|
||||||
ConcurrencyLimit: 2,
|
|
||||||
QueueCapacity: &noWaiting,
|
|
||||||
}),
|
|
||||||
promptkit.WithProfiles(promptkit.Profile{
|
|
||||||
ID: "gpu-summary",
|
|
||||||
BackendID: "local-gpu",
|
|
||||||
Model: "example-model",
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Use distinct custom IDs when registering multiple local endpoints.
|
Place `backend` in a caller underlay and select `"local-gpu"` from a
|
||||||
Registrations belong to one engine and custom IDs cannot replace built-ins.
|
`ProfileConfig.Backend` pointer or an identified file profile. All backend
|
||||||
The [`Backend`, `LocalBackend`, and `WithBackend` GoDoc](../../backends.go)
|
contributions use the same ConfigKit layering rules, including overlays of
|
||||||
defines exact construction, validation, copying, uniqueness, concurrency, and
|
maintained definitions. The [`BackendConfig` and `LocalBackend` GoDoc](../../backends.go)
|
||||||
request-default behavior.
|
defines exact omission and construction behavior.
|
||||||
|
|
||||||
Both file-backed and in-memory profiles select a registration through
|
Both file-backed and programmatic profiles select a backend through
|
||||||
`backend` or `Profile.BackendID`. Profile and request endpoint overrides retain
|
`backend` or `ProfileConfig.Backend`. Profile and request endpoint overrides retain
|
||||||
that routing and capacity identity. `PreparedRun.SelectedBackendID`,
|
that routing and capacity identity. `PreparedRun.SelectedBackendID`,
|
||||||
`RunResult.SelectedBackendID`, and the effective `ExecutionTarget.BackendID`
|
`RunResult.SelectedBackendID`, and the effective `ExecutionTarget.BackendID`
|
||||||
expose it to consumers and injected model clients. Endpoint-only profiles
|
expose it to consumers and injected model clients. Endpoint-only profiles
|
||||||
@@ -508,7 +831,7 @@ remain supported and expose an empty backend ID.
|
|||||||
|
|
||||||
### Limit Backend Concurrency
|
### Limit Backend Concurrency
|
||||||
|
|
||||||
Set `Backend.ConcurrencyLimit` when a backend needs protection from too many
|
Set `BackendConfig.ConcurrencyLimit` when a backend needs protection from too many
|
||||||
simultaneous model calls. Leaving `QueueCapacity` nil, as in the local-backend
|
simultaneous model calls. Leaving `QueueCapacity` nil, as in the local-backend
|
||||||
example above, selects the default waiting capacity of 1024.
|
example above, selects the default waiting capacity of 1024.
|
||||||
|
|
||||||
@@ -517,24 +840,23 @@ zero:
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
noWaiting := 0
|
noWaiting := 0
|
||||||
backend := promptkit.Backend{
|
backend := promptkit.BackendConfig{
|
||||||
ID: "local-gpu",
|
Endpoint: &endpoint,
|
||||||
Endpoint: "http://gpu-host:8000/v1",
|
ConcurrencyLimit: &limit,
|
||||||
ConcurrencyLimit: 2,
|
QueueCapacity: &noWaiting,
|
||||||
QueueCapacity: &noWaiting,
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The pointer distinguishes an explicit zero from omission. Keep using keyed
|
The pointer distinguishes an explicit zero from omission. Use identified
|
||||||
`Backend` literals so additive configuration fields remain source-compatible.
|
`BackendConfig` literals inside `BackendSource` for programmatic definitions.
|
||||||
Capacity belongs to one engine and the selected backend ID; endpoint-only
|
Capacity belongs to one engine and the selected backend ID; endpoint-only
|
||||||
profiles and custom backends without a configured limit remain unrestricted.
|
profiles and custom backends without a configured limit remain unrestricted.
|
||||||
Exact validation, defaulting, ownership, and concurrency semantics belong to
|
Exact validation, defaulting, ownership, and concurrency semantics belong to
|
||||||
the [`Backend` GoDoc](../../backends.go).
|
the [`BackendConfig` GoDoc](../../backends.go).
|
||||||
|
|
||||||
## Credentials
|
## Credentials
|
||||||
|
|
||||||
File-backed profiles name an environment variable; in-memory profiles can
|
File-backed and programmatic profiles can name an environment variable or
|
||||||
require a direct request key. Direct keys are request-scoped and are excluded
|
require a direct request key. Direct keys are request-scoped and are excluded
|
||||||
from supported JSON values and the package's `String` and `GoString`
|
from supported JSON values and the package's `String` and `GoString`
|
||||||
summaries. The exact precedence and redaction guarantees belong to
|
summaries. The exact precedence and redaction guarantees belong to
|
||||||
@@ -568,12 +890,13 @@ the application policy that motivated the injection.
|
|||||||
|
|
||||||
Use `errors.Is` with the
|
Use `errors.Is` with the
|
||||||
[public error sentinels and operation GoDoc](../../engine.go). The declarations
|
[public error sentinels and operation GoDoc](../../engine.go). The declarations
|
||||||
distinguish invalid construction, invalid requests, absent sources,
|
distinguish invalid construction, invalid requests, absent selections,
|
||||||
source-loading failures, collaborator failures, and operational validation
|
collaborator failures, and operational validation
|
||||||
failures. Specific request conditions may also match the broader
|
failures. Specific request conditions may also match the broader
|
||||||
`ErrInvalidRequest`, and injected collaborator identities are preserved where
|
`ErrInvalidRequest`, and injected collaborator identities are preserved where
|
||||||
documented. Invalid or duplicate backend registrations match
|
documented. Invalid effective definitions, including unknown backend
|
||||||
`ErrInvalidConfig`; selecting an unknown backend matches `ErrProfileLoad`.
|
references, match `ErrInvalidConfig` during construction. ConfigKit load
|
||||||
|
errors remain available through `errors.As`.
|
||||||
|
|
||||||
When a limited backend has admitted all active and waiting calls, handle
|
When a limited backend has admitted all active and waiting calls, handle
|
||||||
`ErrCapacityExceeded` separately from request errors and provider failures:
|
`ErrCapacityExceeded` separately from request errors and provider failures:
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
# Development
|
# Development
|
||||||
|
|
||||||
This is the contributor entry point for Promptkit, a reusable Go library. All
|
This is the contributor entry point for Promptkit, a reusable Go library.
|
||||||
contributors must read the
|
Use the Policy Reading Guide below to select policies applicable to the task.
|
||||||
[architecture policy](policy/architecture.md) before making changes. Agents must
|
|
||||||
also follow the [agent policy](policy/agents.md) before recommendations or work.
|
|
||||||
|
|
||||||
## Initial Orientation
|
## Initial Orientation
|
||||||
|
|
||||||
@@ -16,18 +14,27 @@ Before starting work:
|
|||||||
4. keep documentation limited to implemented behavior unless an accepted
|
4. keep documentation limited to implemented behavior unless an accepted
|
||||||
decision or temporary roadmap explicitly owns future work.
|
decision or temporary roadmap explicitly owns future work.
|
||||||
|
|
||||||
Start with:
|
## Policy Reading Guide
|
||||||
|
|
||||||
- the [architecture policy](policy/architecture.md) for library boundaries,
|
| Policy | Read before… |
|
||||||
dependency direction, invariants, and non-goals;
|
| --- | --- |
|
||||||
- the [internal component overview](internal/overview.md) for the current
|
| [Policy set provenance](policy/meta.md) | Adopting policies, checking upstream revisions, or updating the adopted policy set. |
|
||||||
package and component inventory;
|
| [Agents](policy/agents.md) | Recommending, planning, or implementing repository changes. |
|
||||||
- the [documentation policy](policy/documentation.md) before changing
|
| [Architecture](policy/architecture.md) | Planning or changing library code or its structure. |
|
||||||
documentation;
|
| [Testing](policy/testing.md) | Selecting validation for code changes, or adding, changing, removing, or evaluating tests. |
|
||||||
- the [testing policy](policy/testing.md) before adding, rewriting, or deleting
|
| [Documentation](policy/documentation.md) | Writing, revising, or reorganizing documentation. |
|
||||||
tests; and
|
| [OSS](policy/oss.md) | Adding or updating dependencies or incorporated third-party material, or changing licensing or attribution. |
|
||||||
- the [release policy](policy/release.md) and [release procedure](release.md)
|
| [Release](policy/release.md) | Preparing or publishing a release, changing release procedures, or making versioning and compatibility decisions. |
|
||||||
for version and publication work.
|
|
||||||
|
- Read applicable policies before making decisions they govern; multiple policies
|
||||||
|
may apply.
|
||||||
|
- Select policies using the task triggers above. Follow cross-references to
|
||||||
|
relevant sections when needed to resolve a concrete question.
|
||||||
|
- Reuse policies already available in context; reread when changed or no longer
|
||||||
|
available.
|
||||||
|
- Expand reading when the task's scope expands.
|
||||||
|
- When revising a policy itself, read that policy and any provisions elsewhere
|
||||||
|
affected by the revision.
|
||||||
|
|
||||||
## Task-Specific Reading Guide
|
## Task-Specific Reading Guide
|
||||||
|
|
||||||
@@ -36,13 +43,14 @@ Start with:
|
|||||||
| Root public API | The [architecture policy](policy/architecture.md), [consumer guide](consumers/pkg-promptkit.md), [testing policy](policy/testing.md), and existing GoDoc. |
|
| Root public API | The [architecture policy](policy/architecture.md), [consumer guide](consumers/pkg-promptkit.md), [testing policy](policy/testing.md), and existing GoDoc. |
|
||||||
| Prompt, profile, or schema formats | The [framework format reference](formats.md), owning parser or validator package, and [documentation policy](policy/documentation.md). |
|
| Prompt, profile, or schema formats | The [framework format reference](formats.md), owning parser or validator package, and [documentation policy](policy/documentation.md). |
|
||||||
| Source loading or validation | The [framework format reference](formats.md), [internal source document](internal/sources.md), and owning package tests. |
|
| Source loading or validation | The [framework format reference](formats.md), [internal source document](internal/sources.md), and owning package tests. |
|
||||||
| Maintained external catalogs or built-ins | The [internal source document](internal/sources.md), [framework format reference](formats.md), [testing policy](policy/testing.md), both catalog module repositories, each catalog's release procedure, and the selected module versions in `go.mod`. |
|
| Caller-supplied configuration sources | The [internal source document](internal/sources.md), [framework format reference](formats.md), and [testing policy](policy/testing.md). |
|
||||||
| Model-client behavior | The [OpenAI-compatible integration contract](integrations/openai-compatible-chat.md), [internal model-client document](internal/llm.md), and owning package tests. |
|
| Model-client behavior | The [OpenAI-compatible integration contract](integrations/openai-compatible-chat.md), [internal model-client document](internal/llm.md), and owning package tests. |
|
||||||
| Internal package implementation | The [architecture policy](policy/architecture.md), [internal component overview](internal/overview.md), and focused internal document listed for that package. |
|
| Internal package implementation | The [architecture policy](policy/architecture.md), [internal component overview](internal/overview.md), and focused internal document listed for that package. |
|
||||||
| Tests or test fixtures | The [testing policy](policy/testing.md), owning package, and focused internal document listed by the component overview. |
|
| Tests or test fixtures | The [testing policy](policy/testing.md), owning package, and focused internal document listed by the component overview. |
|
||||||
| Maintained example | The [preparation](../examples/go-library/prepare/main.go), [execution](../examples/go-library/run/main.go), and [observation](../examples/go-library/observe/main.go) examples, [consumer guide](consumers/pkg-promptkit.md), [framework format reference](formats.md), and [documentation policy](policy/documentation.md). |
|
| Maintained example | The [preparation](../examples/go-library/prepare/main.go), [execution](../examples/go-library/run/main.go), and [observation](../examples/go-library/observe/main.go) examples, [consumer guide](consumers/pkg-promptkit.md), [framework format reference](formats.md), and [documentation policy](policy/documentation.md). |
|
||||||
| Documentation | The [documentation policy](policy/documentation.md) and canonical owner of every affected contract. |
|
| Documentation | The [documentation policy](policy/documentation.md) and canonical owner of every affected contract. |
|
||||||
| Release preparation or publication | The [release policy](policy/release.md) and [release procedure](release.md). |
|
| Licensing, dependencies, copied material, or attribution | The [OSS policy](policy/oss.md), [project grant](../COPYRIGHT), [license texts](../LICENSE), and [attribution entries](oss/attribution.md); inspect the selected upstream source's notices. |
|
||||||
|
| Release preparation or publication, including release notes | The [release policy](policy/release.md), [release procedure](release.md), and [OSS discoverability rules](policy/oss.md#discoverability). |
|
||||||
|
|
||||||
For cross-cutting changes, follow every applicable row. Do not create
|
For cross-cutting changes, follow every applicable row. Do not create
|
||||||
placeholder documents for packages, APIs, or integrations that do not yet
|
placeholder documents for packages, APIs, or integrations that do not yet
|
||||||
@@ -51,37 +59,42 @@ exist.
|
|||||||
## Maintainer Validation
|
## Maintainer Validation
|
||||||
|
|
||||||
For ordinary work, select validation proportionate to the change. Do not repeat
|
For ordinary work, select validation proportionate to the change. Do not repeat
|
||||||
successful checks for unchanged content merely to create a commit. The full
|
successful checks for unchanged content merely to create a commit. During
|
||||||
release-candidate checks have one executable entry point from the repository root:
|
development, run relevant Go tests and vet, format changed Go files, and execute
|
||||||
|
the maintained examples when they are affected:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./scripts/release-check.sh
|
go test ./...
|
||||||
|
go vet ./...
|
||||||
|
gofmt -w path/to/changed.go
|
||||||
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
||||||
The [script](../scripts/release-check.sh) owns the executable checks:
|
Release candidates use `./scripts/release.sh VERSION`, which runs the selected
|
||||||
race-enabled tests, vet, the three offline consumer examples with semantic output
|
profile checks and publishes the tag. The [release procedure](release.md)
|
||||||
assertions, tracked Go formatting, local Markdown links, module/workspace
|
describes its requirements and recovery. For an optional diagnostic of a
|
||||||
hygiene, whitespace, and the credential-pattern scan. It does not publish,
|
committed candidate without publication, run `./scripts/releasekit.sh check
|
||||||
change files, validate release-note structure, or retire roadmaps. It requires Go,
|
VERSION`; this is not a required pre-publication step.
|
||||||
Git, a POSIX shell, and Python 3. Tests and examples require no provider
|
|
||||||
|
The example workflows are covered by the Go suite; use
|
||||||
|
`go test ./examples/go-library/...` to run them separately during development.
|
||||||
|
The focused `python3 scripts/test-release.py` command exercises the release
|
||||||
|
tooling against disposable local repositories; use it when changing that
|
||||||
|
tooling, not as a recurring release gate. Tests and examples require no provider
|
||||||
credentials or live inference calls.
|
credentials or live inference calls.
|
||||||
|
|
||||||
The script can run on intended uncommitted changes during development.
|
Ordinary validation can run on intended uncommitted work. Release validation
|
||||||
Formatting and documentation checks use tracked files; stage intended new
|
uses committed candidate contents and the selected local Go installation.
|
||||||
files before final validation so those checks include them. The release
|
|
||||||
procedure separately enforces a clean, committed candidate.
|
|
||||||
|
|
||||||
### Change Review
|
### Change Review
|
||||||
|
|
||||||
Inspect `git status --short --untracked-files=all` and the complete diff before
|
Inspect `git status --short --untracked-files=all` and the complete diff before
|
||||||
accepting a change. Reject unintended files, credentials, private data,
|
accepting a change. Reject unintended files, credentials, private data,
|
||||||
generated binaries, test output, downloaded assets, and template residue.
|
generated binaries, test output, downloaded assets, and template residue.
|
||||||
The automated credential scan does not replace review of the actual change.
|
|
||||||
|
|
||||||
For releases, follow the [release procedure](release.md). Its publication
|
For releases, follow the [release procedure](release.md). Do not repeat its
|
||||||
command runs the full local checks once on the final committed candidate and
|
candidate checks as a separate sign-off. Roadmap retirement remains preparation
|
||||||
automates release-note structure and Git checks. Do not repeat ordinary change
|
housekeeping.
|
||||||
review as a release sign-off. Roadmap retirement remains preparation housekeeping.
|
|
||||||
|
|
||||||
Follow the [agent commit policy](policy/agents.md#git-commit-policy) for local
|
Follow the [agent commit policy](policy/agents.md#git-commit-policy) for local
|
||||||
commits. Ordinary commits may leave unrelated work in the workspace; preserve
|
commits. Ordinary commits may leave unrelated work in the workspace; preserve
|
||||||
|
|||||||
704
docs/formats.md
704
docs/formats.md
@@ -2,394 +2,442 @@
|
|||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
This document is the canonical contract for Promptkit prompt-definition,
|
This is the canonical reference for Promptkit's backend, profile, prompt, and
|
||||||
profile, and schema files. The [Go package consumer guide](consumers/pkg-promptkit.md)
|
schema definitions. The [Go consumer guide](consumers/pkg-promptkit.md) shows
|
||||||
explains how to select these sources and invoke the engine. The
|
how to select sources and use an engine; the
|
||||||
[OpenAI-compatible integration contract](integrations/openai-compatible-chat.md)
|
[OpenAI-compatible integration contract](integrations/openai-compatible-chat.md)
|
||||||
owns the resulting outbound wire behavior.
|
owns outbound wire behavior. [ConfigKit](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.1.0/docs/config.md)
|
||||||
|
owns the generic YAML grammar, imports, layering, inheritance, and diagnostics.
|
||||||
|
This reference describes what Promptkit does with the final effective values.
|
||||||
|
|
||||||
Prompt and profile sources recursively discover files ending in `.yaml` or
|
A configuration document may contain top-level `backends`, `profiles`, and
|
||||||
`.yml`. Each prompt-definition and profile file contains exactly one YAML
|
`prompts` lists, plus a `collections` mapping of namespaced resource groups.
|
||||||
document; comments and trailing whitespace are allowed. YAML decoding is
|
Each resource has an explicit literal `id`, not a filename-derived identity.
|
||||||
strict: unknown fields are errors for the selected definition. Definitions are
|
IDs and references must be nonblank and remain exact: Promptkit does not trim or
|
||||||
selected by their YAML `id`, not their file name or directory.
|
re-key them. Unknown fields are errors. Promptkit installs a default-on bundled
|
||||||
|
OpenRouter underlay; callers still explicitly select a profile.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
backends:
|
||||||
|
- id: local
|
||||||
|
endpoint: http://localhost:8000/v1
|
||||||
|
concurrency_limit: 2
|
||||||
|
profiles:
|
||||||
|
- id: local-summary
|
||||||
|
backend: local
|
||||||
|
model: example-model
|
||||||
|
prompts:
|
||||||
|
- id: meeting.summary
|
||||||
|
default_profile: local-summary
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: Summarize this note.
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
collections:
|
||||||
|
support:
|
||||||
|
release_version: "1.4.0"
|
||||||
|
backends:
|
||||||
|
- id: offline
|
||||||
|
endpoint: https://example.invalid/v1
|
||||||
|
profiles:
|
||||||
|
- id: base
|
||||||
|
backend: support/offline
|
||||||
|
model: offline-model
|
||||||
|
- id: fast
|
||||||
|
extends: {id: base}
|
||||||
|
timeout_seconds: 30
|
||||||
|
prompts:
|
||||||
|
- id: answer
|
||||||
|
default_profile: support/fast
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: Answer the selected caller input.
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
```
|
||||||
|
|
||||||
|
## Source Headers, Collections, And Resource IDs
|
||||||
|
|
||||||
|
An input may omit a source header. When present, the source-root `version` is
|
||||||
|
reserved for ConfigKit's specification header and accepts only the exact string
|
||||||
|
`"2"` or strict decimal integer `2`; null and other values are invalid. This
|
||||||
|
header is distinct from a collection's `release_version` and ConfigKit's own
|
||||||
|
software version. ConfigKit checks and consumes accepted headers on non-main
|
||||||
|
inputs; a declaration on the main input remains visible in explicit
|
||||||
|
configuration inspection. Headerless inputs still undergo ordinary final-value
|
||||||
|
validation. ConfigKit owns generic import and source-history behavior.
|
||||||
|
For a standalone prompt item, a root `version` is only a source header, not
|
||||||
|
prompt metadata.
|
||||||
|
|
||||||
|
The root resource lists contain unqualified IDs. Each `collections` entry
|
||||||
|
has a namespace key and may contain `release_version` plus the same three
|
||||||
|
resource lists. Collection resource IDs are local keys; their runtime IDs
|
||||||
|
are `<namespace>/<local-key>`. Root and collection keys may be the same. A
|
||||||
|
namespace and local key must be nonblank and slash-free, and references must
|
||||||
|
name their intended exact ID; mounting a source does not rewrite references.
|
||||||
|
For example, the `support/fast` profile above stores `support/offline` as its
|
||||||
|
backend ID. `extends: {id: base}` refers to a sibling in the same resource
|
||||||
|
list. It is not a qualified or cross-collection reference.
|
||||||
|
|
||||||
|
`release_version` is an optional opaque collection label. Absent or null means
|
||||||
|
no label; an explicitly empty string is retained. It does not select a resource
|
||||||
|
or change execution. Runtime lookups
|
||||||
|
use the captured effective resource indexes. `InspectConfiguration` is an
|
||||||
|
explicit operator view of resolved settings and provenance. Selected field
|
||||||
|
normalizations and the derived queue default are recorded in that tree, with
|
||||||
|
source history retained. Request-time completion remains separate. Source
|
||||||
|
history is not a second runtime resolver, and the
|
||||||
|
retained effective shape is not flattened or rewritten into those indexes.
|
||||||
|
|
||||||
|
For examples of explicitly selecting native collection inputs and inspecting
|
||||||
|
inventories, see the [consumer guide](consumers/pkg-promptkit.md) and its
|
||||||
|
[maintained preparation example](../examples/go-library/prepare/main.go). For
|
||||||
|
breaking identity and prompt-selection changes, see the
|
||||||
|
[configuration migration guide](consumers/configuration-migration.md).
|
||||||
|
|
||||||
|
Every effective backend, profile, and prompt definition is validated when
|
||||||
|
`NewEngine` constructs the engine, even if it is never selected. A profile may
|
||||||
|
be partial: an endpoint and model need only be complete after request overrides
|
||||||
|
are applied. A prompt's default profile similarly need not be runnable without
|
||||||
|
an overriding request. Missing request data, artifacts, schemas, credentials,
|
||||||
|
and provider work remain runtime concerns.
|
||||||
|
|
||||||
|
The engine captures effective definitions, referenced message-file contents,
|
||||||
|
and static template syntax at construction. It does not reread configuration or
|
||||||
|
message files on ordinary runs; construct a new engine to observe edits.
|
||||||
|
Schema and request artifact reads still happen during preparation. Use
|
||||||
|
`InspectConfiguration` for an explicit effective-configuration or provenance
|
||||||
|
report, `InspectProfile` for backend-completed profile settings, and preparation
|
||||||
|
for request-specific rendered details. For identity discovery before inspection,
|
||||||
|
see the consumer guide's [resource and collection listing](consumers/pkg-promptkit.md#discover-available-resources-and-collections).
|
||||||
|
|
||||||
## Prompt Definitions
|
## Prompt Definitions
|
||||||
|
|
||||||
A prompt definition describes inputs, Go-template messages, an optional
|
The `prompts` list contains definitions selected by their literal IDs:
|
||||||
default profile, and an output contract.
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
id: meeting.summary
|
prompts:
|
||||||
version: "1.0.0"
|
- id: meeting.summary
|
||||||
default_profile: local-summary
|
default_profile: local-summary
|
||||||
description: Summarize a synthetic meeting note.
|
description: Summarize a synthetic meeting note.
|
||||||
session_id: '{{.session}}'
|
session_id: '{{.session}}'
|
||||||
inputs:
|
inputs:
|
||||||
- name: note
|
- name: note
|
||||||
required: true
|
required: true
|
||||||
content_type: text/plain
|
content_type: text/plain
|
||||||
description: Meeting note to summarize.
|
description: Meeting note to summarize.
|
||||||
messages:
|
messages:
|
||||||
- role: system
|
- role: system
|
||||||
content: Return a concise summary.
|
content: Return a concise summary.
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
ttl: 1h
|
ttl: 1h
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./summary.user.md
|
content_file: ./summary.user.md
|
||||||
output:
|
output:
|
||||||
format: markdown
|
format: markdown
|
||||||
validation_mode: basic
|
validation_mode: basic
|
||||||
repair_attempts: 0
|
repair_attempts: 0
|
||||||
```
|
```
|
||||||
|
|
||||||
| Field | Required | Meaning |
|
`messages` must be nonempty and `output` must be a valid mapping. Optional
|
||||||
| --- | --- | --- |
|
`default_profile`, `description`, `session_id`, `inputs`, and cache-control
|
||||||
| `id` | yes | Non-empty prompt identifier used by `RunRequest.PromptID`. |
|
fields may be absent or cleared with null where appropriate. A null whole
|
||||||
| `version` | yes | Non-empty version selected by an optional `RunRequest.PromptVersion`. |
|
definition, required role, output format, or validation mode is invalid.
|
||||||
| `default_profile` | no | Non-empty profile ID used when the request omits `ProfileID`. |
|
`required` and `repair_attempts` default to false and zero, respectively, and
|
||||||
| `description` | no | Human-readable description. |
|
are not nullable. `InspectPrompt` selects the same captured definition without
|
||||||
| `session_id` | no | Go template rendered from request variables and input helpers. |
|
rendering or resolving a profile.
|
||||||
| `inputs` | no | Declared input metadata. |
|
|
||||||
| `messages` | yes | One or more chat-message templates. |
|
|
||||||
| `output` | yes | Output format and validation settings. |
|
|
||||||
|
|
||||||
When a request omits a version, the selected prompt ID must identify exactly
|
|
||||||
one definition. When it supplies a version, the ID and version pair must be
|
|
||||||
unique.
|
|
||||||
|
|
||||||
Exact prompt inspection uses this same configured source, strict decoding,
|
|
||||||
referenced content-file resolution, and ID/version selection. It reports the
|
|
||||||
selected definition's declared metadata without changing the prompt format or
|
|
||||||
executing the definition.
|
|
||||||
|
|
||||||
### Inputs
|
### Inputs
|
||||||
|
|
||||||
Each `inputs` item has these fields:
|
Each `inputs` item may contain `name`, `required`, `content_type`, and
|
||||||
|
`description`. `name` is required, nonblank, and unique within the prompt.
|
||||||
| Field | Required | Meaning |
|
Omitted `required` means false. Null `inputs` means no declarations. Requests
|
||||||
| --- | --- | --- |
|
supply inline or file-backed `ArtifactRef` values; declared required inputs
|
||||||
| `name` | yes | Non-empty name used by the request input map and `input` template helper. Names must be unique. |
|
must be present. A template reference also needs its named input, even when
|
||||||
| `required` | no | When true, preparation fails if the request omits the input. The default is false. |
|
undeclared. Extra request inputs are allowed.
|
||||||
| `content_type` | no | Expected media-type metadata. |
|
|
||||||
| `description` | no | Human-readable input description. |
|
|
||||||
|
|
||||||
Requests supply inputs as inline or file-backed `ArtifactRef` values. Declared
|
|
||||||
required inputs must be present. A template reference also requires the named
|
|
||||||
input to exist, whether or not it was declared. Extra request inputs are
|
|
||||||
allowed.
|
|
||||||
|
|
||||||
### Messages And Templates
|
### Messages And Templates
|
||||||
|
|
||||||
Each message has a `role` that Promptkit trims and lowercases. It must then be
|
Each message needs a role and exactly one nonnull `content` or `content_file`.
|
||||||
exactly one of `developer`, `system`, `user`, or `assistant`; blank, custom,
|
An explicitly empty inline `content` is valid. Null clears either content
|
||||||
`tool`, and `function` roles are invalid. This intentionally tightens the
|
selection. Roles are trimmed and lowercased, then must be `developer`,
|
||||||
previous nonblank-string rule. Consumers migrating to the next minor release
|
`system`, `user`, or `assistant`. Static template syntax is checked during
|
||||||
must update any nonstandard prompt-definition roles before upgrading.
|
construction; missing variables and named inputs fail when rendered.
|
||||||
|
Request variables are template data (`{{.audience}}`), and
|
||||||
|
`{{input "note"}}` renders an input body.
|
||||||
|
|
||||||
Each message also has exactly one of:
|
`content_file` uses the effective field's own source context, including when
|
||||||
|
inherited or imported. Its relative path is resolved against that source's
|
||||||
|
base directory within its rooted filesystem. Absolute paths and lexical
|
||||||
|
escapes from the selected root fail. Host-file sources also prevent symlink
|
||||||
|
escapes. Promptkit preserves the path text, including meaningful whitespace,
|
||||||
|
after checking that it is not blank. A source without a unique usable resource
|
||||||
|
context must supply one explicitly or override the field. Content is captured
|
||||||
|
at construction, not reopened for each request.
|
||||||
|
|
||||||
- `content`, containing an inline Go template; or
|
Optional `session_id` uses the same template variables and input helper. Its
|
||||||
- `content_file`, naming a file whose contents are the Go template.
|
rendered value is trimmed, omitted if empty, and limited to 256 Unicode code
|
||||||
|
points. A nonblank direct request session ID bypasses the template.
|
||||||
|
|
||||||
`content_file` must be a relative path. It resolves from the directory that
|
`InspectRequirements` can enumerate recognizable dependencies from captured
|
||||||
contains the prompt file and must remain within the configured prompt source
|
templates before artifact content is available. Its conservative syntax
|
||||||
root; parent components are allowed only when the resolved target remains
|
coverage does not change template validity or rendering; see the [consumer
|
||||||
inside that root. Absolute paths and paths that escape the root are rejected.
|
guide](consumers/pkg-promptkit.md#inspect-requirements-before-preparation) and
|
||||||
Operating-system directory and single-file sources also reject symlink targets
|
the method's [GoDoc](../requirements.go) for that inspection boundary.
|
||||||
outside the root, while injected `fs.FS` sources apply containment in that
|
|
||||||
filesystem's relative path namespace. For `WithPromptFile`, the source root is
|
|
||||||
the directory containing the selected prompt file. Promptkit uses the parsed
|
|
||||||
path text exactly after checking separately that it is not blank, so leading
|
|
||||||
and trailing whitespace can name real filesystem entries.
|
|
||||||
|
|
||||||
Request variables are the template data, so a variable named `audience` is
|
|
||||||
referenced as `{{.audience}}`. The `{{input "note"}}` helper renders the body
|
|
||||||
of a named input. Missing variables and input references are errors.
|
|
||||||
|
|
||||||
The optional `session_id` uses the same template data and input helper. Its
|
|
||||||
rendered value is trimmed, omitted when empty, and limited to 256 Unicode code
|
|
||||||
points. A nonblank direct request session ID bypasses this template completely;
|
|
||||||
a blank direct value leaves the template behavior unchanged.
|
|
||||||
|
|
||||||
### Cache Control
|
### Cache Control
|
||||||
|
|
||||||
`cache_control` is optional and has these fields:
|
Optional `cache_control` has required `type: ephemeral` and optional `ttl`,
|
||||||
|
which may be empty or `1h`. Promptkit retains this metadata on rendered
|
||||||
| Field | Required | Values |
|
messages; the integration contract defines its wire representation.
|
||||||
| --- | --- | --- |
|
|
||||||
| `type` | yes | `ephemeral` |
|
|
||||||
| `ttl` | no | Empty or `1h` |
|
|
||||||
|
|
||||||
Promptkit preserves cache-control metadata on the rendered message. The
|
|
||||||
outbound integration determines its wire representation.
|
|
||||||
|
|
||||||
### Output Contract
|
### Output Contract
|
||||||
|
|
||||||
| Field | Required | Values or behavior |
|
| Field | Required | Values or behavior |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `format` | yes | `text`, `markdown`, or `json`. |
|
| `format` | yes | `text`, `markdown`, or `json` |
|
||||||
| `validation_mode` | yes | `none`, `basic`, `json`, or `json_schema`. |
|
| `validation_mode` | yes | `none`, `basic`, `json`, or `json_schema` |
|
||||||
| `schema_path` | for `json_schema` | Path to a schema in the configured schema source. |
|
| `schema_path` | for `json_schema` | Path in the selected schema source |
|
||||||
| `repair_attempts` | no | Integer from zero through three; omitted means zero. A positive value requires `basic`, `json`, or `json_schema` validation. |
|
| `repair_attempts` | no | Integer 0–3; default 0. Positive values need `basic`, `json`, or `json_schema` |
|
||||||
|
|
||||||
The validation modes behave as follows:
|
`none` skips content validation; `basic` requires nonempty content; `json`
|
||||||
|
accepts any valid JSON value; `json_schema` checks the selected schema.
|
||||||
|
`format` determines output artifact metadata. JSON Schema mode also supplies
|
||||||
|
structured-output metadata to compatible clients. Plain `json` does not
|
||||||
|
request a provider-native JSON-object constraint. A positive repair budget
|
||||||
|
counts additional generations after failed validation; exhaustion returns the
|
||||||
|
last candidate and its failed validation result. Operational errors remain
|
||||||
|
errors. A request-level `OutputContract` replaces the whole configured one,
|
||||||
|
not individual fields; an empty request format defaults to `text`.
|
||||||
|
|
||||||
- `none` skips content validation;
|
## Backend Definitions
|
||||||
- `basic` requires non-empty generated content;
|
|
||||||
- `json` requires valid JSON; and
|
|
||||||
- `json_schema` requires valid JSON that satisfies the selected schema.
|
|
||||||
|
|
||||||
`format` controls output artifact metadata. JSON Schema mode also supplies the
|
Backend definitions are supplied through caller-selected configuration
|
||||||
schema to compatible model clients as structured-output metadata. Plain `json`
|
sources. The conventional backend IDs are names only; PromptKit does not
|
||||||
validation accepts every valid JSON value and does not request a provider-native
|
install definitions for them.
|
||||||
JSON-object constraint.
|
|
||||||
|
|
||||||
`repair_attempts` counts additional generation calls after a failed validation.
|
A backend definition may have `endpoint`, `api_key_env`,
|
||||||
Zero is single-pass. With a positive eligible budget, Promptkit stops at the
|
`concurrency_limit`, `queue_capacity`, `extra_params`, and `smoke_tests`. The
|
||||||
first valid candidate. If the budget is exhausted, it returns the final
|
final endpoint is required and must be an absolute HTTP(S) URL with a host, no
|
||||||
candidate and its complete failed validation result; generation and operational
|
user information, query, or fragment. An omitted or null API-key environment
|
||||||
validation failures remain errors. `none` never permits repair.
|
name means no lookup. Omitted/null concurrency is zero (unlimited); a supplied
|
||||||
|
negative value is invalid. For positive concurrency, omitted/null queue
|
||||||
|
capacity defaults to 1024. An explicit queue capacity, including zero,
|
||||||
|
requires positive concurrency and must be nonnegative. Unlimited concurrency
|
||||||
|
has no queue. Provider maps contain JSON-compatible values; null entries are
|
||||||
|
ordinary JSON null. Unknown fields, including a raw `api_key`, fail.
|
||||||
|
|
||||||
A request-level `OutputContract` replaces the complete prompt output contract.
|
`smoke_tests` is optional backend metadata for explicit diagnostic operations.
|
||||||
It does not merge individual fields. If its format is empty, Promptkit uses
|
It may contain `default_profile`, `reachability`, and `authentication`:
|
||||||
`text`.
|
|
||||||
|
|
||||||
## Built-In Backends
|
```yaml
|
||||||
|
backends:
|
||||||
|
- id: openrouter
|
||||||
|
endpoint: https://openrouter.ai/api/v1
|
||||||
|
smoke_tests:
|
||||||
|
default_profile: application-profile
|
||||||
|
reachability:
|
||||||
|
endpoint: https://status.example/ready?check=1
|
||||||
|
authentication:
|
||||||
|
kind: openrouter_key
|
||||||
|
endpoint: https://openrouter.ai/api/v1/key
|
||||||
|
```
|
||||||
|
|
||||||
Every engine provides these reserved OpenAI-compatible backend IDs. Consumers
|
An omitted or null `smoke_tests` block disables these metadata defaults.
|
||||||
must not register either ID with `WithBackend`; exact registration and
|
`default_profile` is preserved as an exact, nonblank profile ID; its existence
|
||||||
reservation behavior belongs to the [`Backend` GoDoc](../backends.go).
|
is checked only if an inference diagnostic selects that default. Omitted/null
|
||||||
|
`default_profile` means there is no inference default. Omitted/null
|
||||||
|
`reachability` or its endpoint uses the effective backend endpoint. An explicit
|
||||||
|
reachability endpoint is trimmed and must be an absolute HTTP(S) URL with a
|
||||||
|
host; unlike the backend base endpoint, its path and query are allowed. Null
|
||||||
|
means fallback, while an explicit blank value is invalid.
|
||||||
|
|
||||||
| ID | Base endpoint | API-key environment variable | Active generation limit | Default queue capacity |
|
Omitted/null `authentication` means no authentication adapter is declared. A
|
||||||
| --- | --- | --- | ---: | ---: |
|
non-null object requires `kind`, which is trimmed and case-normalized; the
|
||||||
| `openrouter` | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` | 16 | 1024 |
|
recognized values are `none` and `openrouter_key`. Its omitted/null endpoint
|
||||||
| `rakestrawhome` | `https://inference.ai.rakestrawhome.com/v1` | `RAKESTRAWHOME_INFERENCE_API_KEY` | 4 | 1024 |
|
uses the adapter's backend-relative destination (`openrouter_key` appends
|
||||||
|
`/key` to the effective backend base); an explicit endpoint follows the same
|
||||||
|
full-URL rules as reachability. `none` declares authentication inapplicable.
|
||||||
|
These values are configuration only: engine construction does not contact a
|
||||||
|
backend.
|
||||||
|
|
||||||
|
These fields are resolved through the ordinary ConfigKit source layers before
|
||||||
|
Promptkit validates the final effective values. Sibling inheritance can overlay
|
||||||
|
individual nested fields; a later same-ID resource replaces the whole record.
|
||||||
|
An explicit null can clear the whole `smoke_tests` block or one of its nested
|
||||||
|
objects. An endpoint set to null uses
|
||||||
|
its documented fallback, while an explicit blank endpoint is invalid. The
|
||||||
|
metadata does not change ordinary generation credential requirements or
|
||||||
|
provider settings.
|
||||||
|
|
||||||
## Profile Definitions
|
## Profile Definitions
|
||||||
|
|
||||||
A profile supplies model execution settings:
|
Profiles have literal IDs and may inherit a sibling parent with
|
||||||
|
`extends`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
id: local-summary
|
profiles:
|
||||||
backend: openrouter
|
- id: local-summary
|
||||||
model: example-model
|
backend: local
|
||||||
temperature: 0.2
|
model: example-model
|
||||||
max_tokens: 500
|
temperature: 0.2
|
||||||
top_p: 0.95
|
max_tokens: 500
|
||||||
timeout_seconds: 90
|
timeout_seconds: 90
|
||||||
service_tier: flex
|
extra_params:
|
||||||
reasoning_effort: medium
|
provider_option: enabled
|
||||||
extra_params:
|
- id: local-summary-fast
|
||||||
provider_option: enabled
|
extends: {id: local-summary}
|
||||||
|
timeout_seconds: 30
|
||||||
|
reasoning_effort: low
|
||||||
```
|
```
|
||||||
|
|
||||||
A derived profile can use a named base and override only the settings it owns:
|
Profiles may also supply `endpoint`, `top_p`, `service_tier`, `api_key_env`,
|
||||||
|
and `api_key_required`. Promptkit does not infer a backend from an endpoint or
|
||||||
```yaml
|
model. A profile with only an endpoint has no backend capacity identity.
|
||||||
id: local-summary-fast
|
Nonblank endpoints follow the backend URL rules; temperature is 0–2, top_p
|
||||||
base_profile: local-summary
|
is 0–1, max_tokens and timeout_seconds are nonnegative. Provider maps accept
|
||||||
timeout_seconds: 30
|
JSON-compatible values with nonempty keys and cannot collide with the built-in
|
||||||
reasoning_effort: low
|
client's [standard request fields](integrations/openai-compatible-chat.md#request-body).
|
||||||
```
|
Credential values do not belong in profiles: raw `api_key` is invalid.
|
||||||
|
|
||||||
| Field | Required | Meaning |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `id` | yes | Profile identifier, trimmed before selection and publication. It must be non-empty after trimming and unique within one source after normalization. |
|
|
||||||
| `base_profile` | no | One optional parent profile ID. A derived profile may inherit target fields from it. |
|
|
||||||
| `backend` | unless `endpoint` is present | Backend registry ID. It is trimmed and registry membership is checked when the profile is prepared or inspected. |
|
|
||||||
| `endpoint` | unless `backend` is present | OpenAI-compatible base URL, including an API version path when required. A nonempty value is trimmed and must be absolute HTTP or HTTPS with a host and without user information, a query, or a fragment. When both connection fields are present, this overrides the backend endpoint without changing backend identity. |
|
|
||||||
| `model` | yes | Non-empty provider model name. |
|
|
||||||
| `temperature` | no | Number from 0 through 2. |
|
|
||||||
| `max_tokens` | no | Integer zero or greater. |
|
|
||||||
| `top_p` | no | Number from 0 through 1. |
|
|
||||||
| `timeout_seconds` | no | Per-generation deadline in whole seconds; integer zero or greater. |
|
|
||||||
| `service_tier` | no | Provider-specific request tier. |
|
|
||||||
| `reasoning_effort` | no | Provider-specific reasoning setting. |
|
|
||||||
| `api_key_env` | no | Optional environment-variable lookup source for an API key. |
|
|
||||||
| `extra_params` | no | JSON-compatible provider-specific outbound fields. |
|
|
||||||
|
|
||||||
Raw `api_key` is prohibited in profile YAML. Store only an environment
|
|
||||||
variable name in `api_key_env`.
|
|
||||||
|
|
||||||
A standalone profile must provide a model and at least one of `backend` or
|
|
||||||
`endpoint`. A derived profile may omit those target fields because its selected
|
|
||||||
base chain can provide them. Local parsing still validates a derived profile's
|
|
||||||
own ID, supplied endpoint, execution-setting bounds, and `extra_params`.
|
|
||||||
|
|
||||||
Promptkit does not infer a backend from a model or endpoint. Endpoint-only
|
|
||||||
profiles remain supported and have no effective backend ID.
|
|
||||||
The engine always provides the built-in `openrouter` and `rakestrawhome` IDs.
|
|
||||||
Consumers can add engine-scoped IDs with
|
|
||||||
[`WithBackend`](../backends.go); exact registration validation belongs to its
|
|
||||||
GoDoc.
|
|
||||||
|
|
||||||
`extra_params` accepts null, booleans, finite numbers, strings, arrays, and
|
|
||||||
objects with string keys. Keys must be non-empty. With the built-in client,
|
|
||||||
they also cannot collide with the standard fields listed in the
|
|
||||||
[outbound request contract](integrations/openai-compatible-chat.md#request-body).
|
|
||||||
Excessively deep or large JSON-shaped values are rejected for safety.
|
|
||||||
|
|
||||||
### Defaults And Overrides
|
### Defaults And Overrides
|
||||||
|
|
||||||
Execution settings resolve in this order:
|
After effective profile inheritance, Promptkit applies the selected backend's
|
||||||
|
defaults once, then request `ExecutionTargetOverride` values. Backend-derived
|
||||||
|
settings are not inheritance input: changing a child profile's backend does
|
||||||
|
not carry the parent's derived endpoint or credential source into it. Changing
|
||||||
|
only a model or backend does not clear unrelated explicitly inherited fields.
|
||||||
|
|
||||||
1. the framework timeout baseline;
|
| Profile field | Absent | Explicit value | Explicit null |
|
||||||
2. the selected backend, when the profile names one;
|
| --- | --- | --- | --- |
|
||||||
3. the selected profile; and
|
| `backend` | No backend | Exact backend ID; blank means none | No backend |
|
||||||
4. request `ExecutionTargetOverride` values.
|
| `endpoint` | Selected backend endpoint | Valid nonblank URL; empty clears | Clear, without backend fallback |
|
||||||
|
| `model` | Unspecified | Nonblank model; empty clears | Unspecified |
|
||||||
|
| `temperature`, `top_p`, `max_tokens` | Unspecified | Valid value, including outbound zero | Suppress outbound control |
|
||||||
|
| `service_tier`, `reasoning_effort` | Unspecified | Normalized value; blank suppresses | Suppress outbound control |
|
||||||
|
| `timeout_seconds` | 600 seconds | Nonnegative; zero disables generation deadline | Reset to 600 seconds |
|
||||||
|
| `api_key_env` | Backend lookup name, if any | Trimmed name; blank disables | Disable backend lookup |
|
||||||
|
| `api_key_required` | False | Boolean, including false | False |
|
||||||
|
| `extra_params` | Backend defaults | Recursively overlay profile map on backend map | Suppress configured provider map |
|
||||||
|
|
||||||
The framework baseline is:
|
An empty mapping does not clear inherited map entries; null clears the whole
|
||||||
|
map. Nested maps merge recursively, while scalar and array values replace.
|
||||||
|
Runtime nonempty `ExtraParams` overrides replace the entire configured map.
|
||||||
|
Request overrides otherwise follow the [public value GoDoc](../types.go).
|
||||||
|
For example, a runtime `extra_params["extends"]` key is ordinary provider
|
||||||
|
data, not a configuration inheritance instruction. An explicit zero timeout
|
||||||
|
does not disable the caller context or HTTP transport timeout.
|
||||||
|
|
||||||
| Setting | Default |
|
### Bundled OpenRouter Configuration
|
||||||
|
|
||||||
|
The default bundle installs root backend `openrouter`, endpoint
|
||||||
|
`https://openrouter.ai/api/v1`, optional credential environment name
|
||||||
|
`OPENROUTER_API_KEY`, and smoke metadata selecting `gpt-luna-latest` with the
|
||||||
|
`openrouter_key` authentication adapter. Existing diagnostic endpoint fallback
|
||||||
|
applies. No prompt, implicit profile selection, or tuning defaults are installed.
|
||||||
|
Disable the entire bundle with `WithBundledOpenRouter(false)`.
|
||||||
|
|
||||||
|
The root profile IDs and their models are:
|
||||||
|
|
||||||
|
| Profile | Model |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `temperature` | Unspecified and omitted from compatible provider requests unless a profile or runtime override selects it. |
|
| `gpt-astra-latest` | `~openai/gpt-astra-latest` |
|
||||||
| `max_tokens` | Unspecified and omitted from compatible provider requests unless a profile or runtime override selects it. |
|
| `gpt-sol-latest` | `~openai/gpt-sol-latest` |
|
||||||
| `top_p` | Unspecified and omitted from compatible provider requests unless a profile or runtime override selects it. |
|
| `gpt-terra-latest` | `~openai/gpt-terra-latest` |
|
||||||
| `timeout_seconds` | `600` |
|
| `gpt-luna-latest` | `~openai/gpt-luna-latest` |
|
||||||
|
| `claude-fable-latest` | `~anthropic/claude-fable-latest` |
|
||||||
|
| `claude-opus-latest` | `~anthropic/claude-opus-latest` |
|
||||||
|
| `claude-sonnet-latest` | `~anthropic/claude-sonnet-latest` |
|
||||||
|
| `claude-haiku-latest` | `~anthropic/claude-haiku-latest` |
|
||||||
|
| `gemini-pro-latest` | `~google/gemini-pro-latest` |
|
||||||
|
| `gemini-flash-latest` | `~google/gemini-flash-latest` |
|
||||||
|
| `auto` | `openrouter/auto` |
|
||||||
|
| `free` | `openrouter/free` |
|
||||||
|
|
||||||
Numeric zero in a file or in-memory profile does not select a numeric value.
|
The profiles `auto-low`, `auto-medium`, `auto-high`, `auto-xhigh`, and
|
||||||
For `temperature`, `max_tokens`, and `top_p`, it leaves the provider control
|
`auto-max` extend `auto` and add an `auto-router` plugin with the corresponding
|
||||||
unspecified. For `timeout_seconds`, it retains the framework deadline. Numeric
|
`cost_tier`. Replacing `auto` changes their inherited backend and model while
|
||||||
request overrides use pointers, so an explicit zero is retained and sent to
|
retaining their plugins. These are root IDs even when a model contains `/`.
|
||||||
compatible providers. In particular, an explicit request `timeout_seconds` of
|
For example, a caller can replace `auto` as a complete record and extend another
|
||||||
zero disables the per-generation deadline while leaving the caller context and
|
bundled profile under a new ID:
|
||||||
transport timeout intact.
|
|
||||||
|
|
||||||
Non-empty profile strings replace backend defaults, and non-empty request
|
```yaml
|
||||||
strings replace both. Request reasoning is the exception: a nil
|
profiles:
|
||||||
`ReasoningEffort` pointer inherits the profile, a pointer to a nonblank string
|
- id: auto
|
||||||
trims and replaces it, and a pointer to a blank string clears it. Backend
|
backend: openrouter
|
||||||
identity is retained when either layer overrides the endpoint, so the override
|
model: openrouter/free
|
||||||
also retains any engine-local capacity policy configured for that backend.
|
- id: my-luna
|
||||||
Capacity configuration belongs to the Go
|
extends: {id: gpt-luna-latest}
|
||||||
[`Backend` API](../backends.go), not prompt or profile YAML. A non-empty
|
timeout_seconds: 60
|
||||||
`extra_params` map at each layer replaces the entire lower-precedence map
|
```
|
||||||
rather than merging keys.
|
|
||||||
The [outbound integration contract](integrations/openai-compatible-chat.md)
|
See OpenRouter's [alias examples](https://openrouter.ai/~openai/gpt-luna-latest),
|
||||||
defines how the effective settings are serialized.
|
[auto router and cost tiers](https://openrouter.ai/docs/guides/routing/routers/auto-router),
|
||||||
|
and [free router](https://openrouter.ai/docs/guides/routing/routers/free-router)
|
||||||
|
for provider behavior and availability.
|
||||||
|
|
||||||
### Source And Profile Precedence
|
### Source And Profile Precedence
|
||||||
|
|
||||||
An explicit request profile ID takes precedence over the prompt's
|
`NewEngine` composes the bundled OpenRouter underlay first by default, then
|
||||||
`default_profile`. If neither is present, preparation fails. Exact profile
|
caller underlays in their given order, followed by an
|
||||||
inspection instead takes one explicit profile ID and does not use a prompt
|
optional main input. Later layers overlay earlier ones. Inputs within one
|
||||||
default.
|
underlay group are strictly composed: conflicting contributions are errors,
|
||||||
|
not priority signals. The directory helpers discover `.yaml` and `.yml` files recursively
|
||||||
|
in sorted order, but that order does not give one file higher precedence.
|
||||||
|
An explicitly selected missing/unreadable root fails; an empty root contributes
|
||||||
|
nothing. Imports are disabled by these helpers unless the caller enables them
|
||||||
|
on an input. Single files can use ConfigKit inputs directly.
|
||||||
|
|
||||||
Profile sources resolve matching IDs in this order:
|
Root backends, profiles, and prompts, as well as collection declarations and
|
||||||
|
their resource lists, follow these same source rules. `CollectionInput`
|
||||||
|
mounts one explicitly selected source at a collection destination and enables
|
||||||
|
ConfigKit imports for that input; it does not discover or select sources.
|
||||||
|
A later same-ID record replaces the lower definition in full. Use a distinct
|
||||||
|
inherited sibling when a variant should retain its parent's fields.
|
||||||
|
A request `ProfileID` takes precedence over a prompt `default_profile`; if
|
||||||
|
neither is selected, preparation fails. Exact profile inspection selects one
|
||||||
|
explicit ID and does not consult a prompt default.
|
||||||
|
|
||||||
1. in-memory profiles supplied with `WithProfiles`;
|
### Resource Inheritance
|
||||||
2. the ordinary configured source selected by a profile file, `fs.FS`, or
|
|
||||||
configured profile directory;
|
|
||||||
3. application fallback profiles supplied with `WithFallbackProfileFS`; and
|
|
||||||
4. Promptkit-owned profiles; and
|
|
||||||
5. maintained external catalog profiles.
|
|
||||||
|
|
||||||
A profile source supplies a complete definition; definitions and their fields
|
ConfigKit resolves `extends: {id: parent}` for backends, profiles, and prompts.
|
||||||
are not merged across sources. A higher-precedence source falls back only when
|
Each child inherits a surviving sibling in the same root or collection list;
|
||||||
the requested profile ID is absent. An invalid matching profile is an error and
|
collection inheritance cannot cross namespaces. Parents are ordinary validated
|
||||||
does not fall back. In-memory `Profile` values follow the same ranges as YAML
|
resources, and profiles may remain partial until request completion. Presence, explicit
|
||||||
profiles. They use `APIKeyRequired` for request-scoped credentials instead of
|
null, and recursively merged maps determine the final fields. Other resource
|
||||||
`api_key_env`. Preparation and exact profile inspection use this same source
|
references are exact IDs and use their qualified form when they select a
|
||||||
precedence.
|
collection resource. There is no Promptkit-specific depth limit or second
|
||||||
|
base-chain resolver. Missing references and cycles fail at construction. For
|
||||||
When a selected definition names `base_profile`, every profile ID in that
|
generic inheritance and layer mechanics, consult
|
||||||
chain is looked up through this same precedence order. A higher-precedence
|
[ConfigKit](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.1.0/docs/config.md).
|
||||||
definition therefore shadows a lower-precedence definition of the same base
|
|
||||||
ID, including a built-in. References are not source-qualified.
|
|
||||||
|
|
||||||
### Profile Inheritance
|
|
||||||
|
|
||||||
Promptkit resolves one linear base chain of at most 32 profiles, including the
|
|
||||||
selected profile. It merges settings from the root base to the selected leaf.
|
|
||||||
The leaf's `id` remains the selected profile identity. Nonblank string fields
|
|
||||||
(`backend`, `endpoint`, `model`, `service_tier`, `reasoning_effort`, and
|
|
||||||
`api_key_env`) and nonzero numeric fields replace inherited values. A nonempty
|
|
||||||
`extra_params` map replaces the complete inherited map rather than merging
|
|
||||||
keys, and `APIKeyRequired: true` remains true through the chain. Backend and
|
|
||||||
endpoint are independent: replacing one does not clear the other.
|
|
||||||
|
|
||||||
There is no profile-level clearing syntax. Blank strings, zero numbers, false,
|
|
||||||
and empty maps remain unspecified and inherit from a base. Use existing
|
|
||||||
presence-aware request overrides where an execution needs an explicit zero or
|
|
||||||
empty reasoning setting.
|
|
||||||
|
|
||||||
An absent directly selected profile reports the ordinary not-found error. Once
|
|
||||||
the selected profile exists, a missing base, cycle, overlong chain, or
|
|
||||||
incomplete resolved target is a profile-load failure. Ordinary operations
|
|
||||||
resolve chains afresh; prepared execution retains the fully resolved target.
|
|
||||||
|
|
||||||
## Built-In Profile Catalog
|
|
||||||
|
|
||||||
Every built-in profile selects one maintained built-in backend and inherits
|
|
||||||
that backend's connection and credential metadata. Profile files do not repeat
|
|
||||||
those values. A configured, application fallback, or in-memory profile with
|
|
||||||
the same profile ID takes precedence.
|
|
||||||
|
|
||||||
Promptkit owns two opt-in profiles. The following definitions are current
|
|
||||||
selections, not automatic defaults:
|
|
||||||
|
|
||||||
| ID | Current definition |
|
|
||||||
| --- | --- |
|
|
||||||
| `promptkit-openrouter-standard` | Inherits `gemini-flash-latest`. |
|
|
||||||
| `promptkit-openrouter-auto` | Selects backend `openrouter` and model `openrouter/auto`. |
|
|
||||||
|
|
||||||
Select either name through an existing request `profile_id` or prompt
|
|
||||||
`default_profile`. Consumers can replace either definition or its inherited
|
|
||||||
base through the ordinary profile-source precedence above. The
|
|
||||||
[release policy](policy/release.md#catalog-profile-compatibility) defines the
|
|
||||||
cross-version guarantee for these names.
|
|
||||||
|
|
||||||
## External Profile Inventories
|
|
||||||
|
|
||||||
The external profile inventory is defined by the selected catalog modules, not
|
|
||||||
duplicated here. Browse the exact data selected by this Promptkit version:
|
|
||||||
|
|
||||||
- [OpenRouter catalog v2.20260922.1 profiles](https://gitea.maximumdirect.net/eric/promptkit-backend-openrouter/src/tag/v2.20260922.1/catalog/profiles)
|
|
||||||
- [Rakestrawhome catalog v2.20260922.0 profiles](https://gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome/src/tag/v2.20260922.0/catalog/profiles)
|
|
||||||
|
|
||||||
External profile entries are volatile across catalog releases. The
|
|
||||||
[release policy](policy/release.md#catalog-profile-compatibility) owns the precise
|
|
||||||
compatibility boundary; a selected module version identifies tested local data,
|
|
||||||
not live provider availability.
|
|
||||||
|
|
||||||
## Schemas
|
## Schemas
|
||||||
|
|
||||||
Schemas are JSON documents selected by a prompt or request
|
Schemas are JSON documents selected by a prompt or request `schema_path`.
|
||||||
`schema_path`. For a directory or `fs.FS` source, paths resolve within the
|
Directory and `fs.FS` schema paths resolve within the configured root;
|
||||||
configured source root. Referenced nested schemas resolve relative to the
|
referenced nested schemas resolve relative to the owning schema document.
|
||||||
owning schema document. `WithSchemaFile` exposes one schema, addressed by its
|
`WithSchemaFile` exposes one schema under its base name. An unreadable,
|
||||||
base name.
|
invalid, or unresolvable schema causes an operational validation error.
|
||||||
|
Valid JSON that fails the schema instead yields a failed validation result.
|
||||||
An unreadable, invalid, or unresolvable schema produces an operational
|
|
||||||
validation error. Generated content that is valid JSON but does not satisfy the
|
|
||||||
schema produces a failed validation result.
|
|
||||||
|
|
||||||
## Credentials
|
## Credentials
|
||||||
|
|
||||||
Credential values belong at the request or environment boundary, never in
|
Only environment-variable names belong in backend/profile definitions, never
|
||||||
prompt, profile, schema, or example files:
|
credential values. A request may supply a direct `APIKey` or an `APIKeyEnv`
|
||||||
|
override. A direct key takes precedence, followed by a request environment
|
||||||
|
source, an explicit profile source, and an inherited backend source. A true
|
||||||
|
profile `api_key_required` suppresses the backend-derived environment source,
|
||||||
|
but an explicit profile or request environment source remains usable. False
|
||||||
|
restores ordinary optional-credential behavior. Missing or blank optional
|
||||||
|
environment values leave the built-in client's Authorization header absent;
|
||||||
|
required availability is checked at preparation and again during prepared
|
||||||
|
execution. Injected clients own their credential-resolution behavior.
|
||||||
|
|
||||||
- a backend or file profile can name an optional environment lookup source
|
Credential values are excluded from JSON results and redacted by public
|
||||||
with `APIKeyEnv` or `api_key_env`;
|
string formatters. ConfigKit inspection redacts credential-shaped dynamic
|
||||||
- an in-memory profile may set `APIKeyRequired` as an explicit local
|
provider keys, but does not make arbitrary prompt/provider content safe to
|
||||||
requirement;
|
disclose. Applications own report disclosure and secret handling.
|
||||||
- a request can provide a direct `APIKey` or override the optional `APIKeyEnv`
|
|
||||||
source; and
|
|
||||||
- a direct request key takes precedence over environment lookup.
|
|
||||||
|
|
||||||
After a direct request key, the credential-source precedence is request
|
|
||||||
`APIKeyEnv`, profile `api_key_env`, then the backend default. An in-memory
|
|
||||||
profile with `APIKeyRequired` clears an inherited backend environment name and
|
|
||||||
requires a direct key unless the request explicitly supplies `APIKeyEnv`.
|
|
||||||
Named environment sources are optional: when the selected source is absent,
|
|
||||||
empty, or whitespace-only, the built-in client omits the `Authorization`
|
|
||||||
header and handles the provider response normally. `APIKeyRequired` is the
|
|
||||||
only explicit local availability requirement. Promptkit validates required
|
|
||||||
credential availability during preparation and rechecks it when a prepared
|
|
||||||
execution runs. Injected clients receive resolved source metadata but define
|
|
||||||
their own credential-resolution behavior.
|
|
||||||
Direct keys are excluded from JSON results and redacted by public string
|
|
||||||
formatters. Environment-variable names may appear in prepared metadata, but
|
|
||||||
their values do not.
|
|
||||||
|
|||||||
@@ -45,6 +45,45 @@ precedence over the backend default. Only the name reaches prepared metadata;
|
|||||||
the environment value is read just before the provider call and is never added
|
the environment value is read just before the provider call and is never added
|
||||||
to the JSON body.
|
to the JSON body.
|
||||||
|
|
||||||
|
## Explicit Smoke Diagnostics
|
||||||
|
|
||||||
|
Promptkit's explicit smoke diagnostics are separate consumer operations; they
|
||||||
|
are not part of ordinary `Run`, the offline examples, or release validation.
|
||||||
|
With the built-in client, inference diagnostics use the same configured client
|
||||||
|
and `/chat/completions` request as generation. Their content cases are private
|
||||||
|
to Promptkit and are not application prompt, schema, or provider protocol
|
||||||
|
extensions. The public
|
||||||
|
[`Engine.SmokeTest` GoDoc](../../smoke_diagnostics.go) owns request selection,
|
||||||
|
report facts, and injected-client behavior.
|
||||||
|
|
||||||
|
HTTP-only diagnostics use the configured built-in HTTP client. Reachability is
|
||||||
|
one unauthenticated `GET` to the selected diagnostic URL; an observed response
|
||||||
|
counts as reachable even when its status is non-2xx. Authentication behavior
|
||||||
|
is selected from backend metadata, not inferred from the host. The
|
||||||
|
`openrouter_key` adapter makes one `GET` to `/key` relative to the effective
|
||||||
|
backend base, unless configuration supplies a full diagnostic endpoint (see
|
||||||
|
the [backend format reference](../formats.md#backend-definitions)). It may send
|
||||||
|
the available bearer credential, but can dispatch without one. A 401 is
|
||||||
|
recognized as rejection. A 200 is recognized as acceptance only when its JSON
|
||||||
|
object has a `data` object containing string `label`, Boolean `is_free_tier`,
|
||||||
|
and numeric `usage`; unrelated fields and account limits are ignored. This is
|
||||||
|
only a minimal identifying subset of OpenRouter's documented
|
||||||
|
[current-key response](https://github.com/OpenRouterTeam/docs/blob/main/api_reference/limits.mdx#checking-your-limits),
|
||||||
|
not validation of the full account document. Other statuses and unrecognized
|
||||||
|
200 responses do not establish acceptance.
|
||||||
|
|
||||||
|
Diagnostic HTTP evidence describes the last request observed at Promptkit's
|
||||||
|
RoundTripper boundary and any response associated with it. It includes the
|
||||||
|
selected destination without URL user information, response status, and
|
||||||
|
credential presence when observed. Redirects can change the last observed
|
||||||
|
destination or strip credentials. This evidence does not prove that bytes
|
||||||
|
reached a peer or reveal dispatch hidden inside a custom transport. Diagnostic
|
||||||
|
reports do not retain headers, credentials, provider
|
||||||
|
bodies, or generated text. A consumer-injected `LLMClient` can support inference
|
||||||
|
diagnostics, but does not establish HTTP status, destination, reachability, or
|
||||||
|
upstream credential-supply facts; Promptkit does not fall back to a built-in
|
||||||
|
transport for HTTP-only diagnostics.
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
The request body always contains `model` and `messages`. The execution
|
The request body always contains `model` and `messages`. The execution
|
||||||
@@ -78,9 +117,8 @@ The client conditionally includes:
|
|||||||
|
|
||||||
The engine resolves backend, profile, and request extra-parameter maps by
|
The engine resolves backend, profile, and request extra-parameter maps by
|
||||||
whole-map replacement rather than key merging. The resulting effective map is
|
whole-map replacement rather than key merging. The resulting effective map is
|
||||||
then merged directly into the top-level body after JSON serialization is
|
merged into the top-level body. Empty keys, values that cannot be encoded, and
|
||||||
verified. Empty keys and collisions with these reserved fields are rejected
|
collisions with these reserved fields are rejected before any provider call:
|
||||||
before any provider call:
|
|
||||||
|
|
||||||
- `model`
|
- `model`
|
||||||
- `session_id`
|
- `session_id`
|
||||||
|
|||||||
@@ -88,6 +88,17 @@ release, including canceled acquisition without dispatch. A retry wait holds
|
|||||||
the logical admission lease but no active permit. Observer callbacks must not
|
the logical admission lease but no active permit. Observer callbacks must not
|
||||||
block on work that requires the same run's admission slot.
|
block on work that requires the same run's admission slot.
|
||||||
|
|
||||||
|
## Smoke-Test Capacity
|
||||||
|
|
||||||
|
The engine's smoke tester shares this manager and generation coordinator with
|
||||||
|
the ordinary runner. Inference diagnostics reserve the same logical admission
|
||||||
|
slot and acquire the same backend-scoped active permits for actual model
|
||||||
|
attempts, including retries. They therefore participate in the same configured
|
||||||
|
backend limits as `Run`. Reachability and authentication HTTP probes do not
|
||||||
|
enter generation admission or consume active permits. The public
|
||||||
|
[`Engine.SmokeTest` GoDoc](../../smoke_diagnostics.go) owns the caller-visible
|
||||||
|
operation and injected-client limits.
|
||||||
|
|
||||||
## Cancellation And Release
|
## Cancellation And Release
|
||||||
|
|
||||||
Admission checks the caller context before reserving a slot. A call canceled
|
Admission checks the caller context before reserving a slot. A call canceled
|
||||||
@@ -127,4 +138,7 @@ cover shared and independent backend pools during retry and repair. The
|
|||||||
own the public prepared-capacity boundary. The
|
own the public prepared-capacity boundary. The
|
||||||
[root error-boundary tests](../../errors_internal_test.go) own preservation of
|
[root error-boundary tests](../../errors_internal_test.go) own preservation of
|
||||||
the public generation category and context identity when generation is
|
the public generation category and context identity when generation is
|
||||||
canceled.
|
canceled. The assembled
|
||||||
|
[smoke diagnostic contract tests](../../smoke_integration_contract_test.go)
|
||||||
|
verify that inference diagnostics share the engine's Run admission and recover
|
||||||
|
capacity after cancellation or failure.
|
||||||
|
|||||||
@@ -41,9 +41,22 @@ original error chain. This keeps injected attempt classifications consistent
|
|||||||
with terminal observations without parsing diagnostic text or retrying injected
|
with terminal observations without parsing diagnostic text or retrying injected
|
||||||
clients.
|
clients.
|
||||||
|
|
||||||
|
The diagnostic path uses the same private attempt and retry implementation as
|
||||||
|
ordinary generation. It retains invocation-local transport, response,
|
||||||
|
generation, usage, capacity-wait, and retry-delay evidence before normal
|
||||||
|
result-discard and cancellation checks. Built-in preparation also supplies its
|
||||||
|
frozen request URL to the evidence path before attempts begin; injected
|
||||||
|
generation has no equivalent HTTP destination. This evidence is returned to
|
||||||
|
diagnostic orchestration; it is not added to public `ExecutionEvent` values or ordinary
|
||||||
|
observer accounting. The [model-client document](llm.md) describes the
|
||||||
|
transport-observation boundary, and the root
|
||||||
|
[`SmokeTestReport` GoDoc](../../smoke_diagnostics.go) owns report semantics.
|
||||||
|
|
||||||
Focused coordinator tests use controlled transport and wait functions. They
|
Focused coordinator tests use controlled transport and wait functions. They
|
||||||
cover eligibility, exhaustion, cancellation, backoff, request freezing, permit
|
cover eligibility, exhaustion, cancellation, backoff, request freezing, permit
|
||||||
release, and the injected-client path. The capacity manager tests own permit
|
release, and the injected-client path. The
|
||||||
|
[evidence tests](../../internal/generation/evidence_test.go) own diagnostic
|
||||||
|
attempt retention and retry evidence. The capacity manager tests own permit
|
||||||
acquisition and idempotent release. Public
|
acquisition and idempotent release. Public
|
||||||
[integration tests](../../reliable_execution_contract_test.go) cover retry,
|
[integration tests](../../reliable_execution_contract_test.go) cover retry,
|
||||||
repair, observation, frozen prepared execution, and shared-backend behavior
|
repair, observation, frozen prepared execution, and shared-backend behavior
|
||||||
|
|||||||
@@ -34,9 +34,13 @@ resolved request target may supply the endpoint. Generation preparation then:
|
|||||||
1. validates shared execution-setting invariants and the final selected base
|
1. validates shared execution-setting invariants and the final selected base
|
||||||
endpoint;
|
endpoint;
|
||||||
2. maps the internal request into the OpenAI-compatible chat payload;
|
2. maps the internal request into the OpenAI-compatible chat payload;
|
||||||
3. validates and merges extra parameters;
|
3. checks extra-parameter keys against empty and reserved names, then merges
|
||||||
|
their values into the payload;
|
||||||
4. composes `/chat/completions` through parsed URL path operations;
|
4. composes `/chat/completions` through parsed URL path operations;
|
||||||
5. resolves authentication and freezes the URL, body, and optional credential.
|
5. encodes the complete body once on success. If encoding fails, extra values
|
||||||
|
are checked individually to retain invalid-request classification; otherwise
|
||||||
|
the complete-request failure remains a request failure;
|
||||||
|
6. resolves authentication and freezes the URL, body, and optional credential.
|
||||||
|
|
||||||
Each execution of that prepared request creates a fresh HTTP request, body
|
Each execution of that prepared request creates a fresh HTTP request, body
|
||||||
reader, and attempt timeout context. It closes the response body and returns
|
reader, and attempt timeout context. It closes the response body and returns
|
||||||
@@ -55,6 +59,33 @@ This client does not schedule retries; the [generation coordinator](generation.m
|
|||||||
does so around its prepared attempts. The client has no tool-call support,
|
does so around its prepared attempts. The client has no tool-call support,
|
||||||
provider catalog, inbound HTTP behavior, or durable session store.
|
provider catalog, inbound HTTP behavior, or durable session store.
|
||||||
|
|
||||||
|
## Diagnostic HTTP Evidence
|
||||||
|
|
||||||
|
Built-in diagnostics wrap the selected HTTP client's `RoundTripper` for one
|
||||||
|
invocation using a shallow client copy, preserving its timeout, redirect policy,
|
||||||
|
cookie jar, and underlying transport without mutating the configured client.
|
||||||
|
Evidence distinguishes a diagnostic dispatch invocation from an observed
|
||||||
|
request and response. A newly observed request replaces the previous
|
||||||
|
destination-specific response evidence, so redirects retain only the last
|
||||||
|
observed hop. A response is recorded before envelope decoding or later context
|
||||||
|
checks can discard a generation result.
|
||||||
|
|
||||||
|
The collector keeps only the last observed destination (without URL user
|
||||||
|
information), status, credential-presence, response-availability, decoded
|
||||||
|
generation, and presence-aware usage facts needed by the diagnostic. It does
|
||||||
|
not retain headers, credentials, raw request or response bodies, or a redirect
|
||||||
|
trace. These are observations at Promptkit's RoundTripper boundary, not proof
|
||||||
|
that bytes reached a peer or that a custom transport did no hidden dispatch.
|
||||||
|
Reachability and named authentication probes use the same configured built-in
|
||||||
|
client but discard their response bodies after local recognition. The
|
||||||
|
[integration contract](../integrations/openai-compatible-chat.md#explicit-smoke-diagnostics)
|
||||||
|
owns their observable HTTP semantics.
|
||||||
|
|
||||||
|
An injected `LLMClient` does not pass through this HTTP boundary. Its returned
|
||||||
|
response can establish response and inference facts, but not an HTTP
|
||||||
|
destination, status, reachability, or upstream credential-supply fact. The
|
||||||
|
facade does not silently install a built-in transport for those diagnostics.
|
||||||
|
|
||||||
## Prepared Generation
|
## Prepared Generation
|
||||||
|
|
||||||
For [`RunPrepared`](../../engine.go), the runner supplies the model client with
|
For [`RunPrepared`](../../engine.go), the runner supplies the model client with
|
||||||
@@ -133,7 +164,10 @@ own configuration, client cloning, deterministic deadline precedence,
|
|||||||
authentication, request and response mapping, malformed data, error identity,
|
authentication, request and response mapping, malformed data, error identity,
|
||||||
cancellation, endpoint selection and composition, pre-transport rejection, and
|
cancellation, endpoint selection and composition, pre-transport rejection, and
|
||||||
bounded single-document successful-response framing, closure, and
|
bounded single-document successful-response framing, closure, and
|
||||||
response-body suppression. The focused
|
response-body suppression. The [HTTP evidence tests](../../internal/llm/http_evidence_test.go)
|
||||||
|
own invocation-local last-hop request/response facts, while the
|
||||||
|
[prepared-generation evidence tests](../../internal/llm/prepared_generation_evidence_test.go)
|
||||||
|
own retention across decoding and cancellation. The focused
|
||||||
[provider HTTP error tests](../../internal/llm/provider_http_error_test.go)
|
[provider HTTP error tests](../../internal/llm/provider_http_error_test.go)
|
||||||
own envelope parsing, normalization, and bounded-reader cases; their
|
own envelope parsing, normalization, and bounded-reader cases; their
|
||||||
[transport tests](../../internal/llm/provider_http_error_transport_test.go)
|
[transport tests](../../internal/llm/provider_http_error_transport_test.go)
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ callback. The caller supplies normalized failure metadata; the recorder never
|
|||||||
parses an error string or receives provider bodies, credentials, headers, or
|
parses an error string or receives provider bodies, credentials, headers, or
|
||||||
other content. Events are values with no mutable containers. Callback panics
|
other content. Events are values with no mutable containers. Callback panics
|
||||||
propagate to the caller.
|
propagate to the caller.
|
||||||
|
Resolved prompt and profile IDs are the exact runtime identities selected for
|
||||||
|
the invocation, including namespace qualification when supplied by an engine
|
||||||
|
view.
|
||||||
Per-operation callbacks are ordered and synchronous; different operations may
|
Per-operation callbacks are ordered and synchronous; different operations may
|
||||||
invoke the same callback concurrently. The recorder marks each measured phase's
|
invoke the same callback concurrently. The recorder marks each measured phase's
|
||||||
end before delivery, so callback work does not inflate provider duration.
|
end before delivery, so callback work does not inflate provider duration.
|
||||||
@@ -41,6 +44,11 @@ attempts above the first in a generation, and repair generations with at least
|
|||||||
one dispatch. A terminal event is an aggregate and must not be added to its
|
one dispatch. A terminal event is an aggregate and must not be added to its
|
||||||
attempt events.
|
attempt events.
|
||||||
|
|
||||||
|
Smoke diagnostics mask inherited execution-recorder state while invoking
|
||||||
|
generation or HTTP collaborators. Their private attempt evidence is assembled
|
||||||
|
into a `SmokeTestReport`; it does not create ordinary run events or enter an
|
||||||
|
enclosing run's observation totals.
|
||||||
|
|
||||||
The [recorder tests](../../internal/observation/recorder_test.go) own usage
|
The [recorder tests](../../internal/observation/recorder_test.go) own usage
|
||||||
presence, phase sums, event ordering, empty-run accounting, identity snapshots,
|
presence, phase sums, event ordering, empty-run accounting, identity snapshots,
|
||||||
and callback panic behavior. The [generation coordinator](generation.md) owns
|
and callback panic behavior. The [generation coordinator](generation.md) owns
|
||||||
|
|||||||
@@ -11,27 +11,27 @@ contributor workflow and validation.
|
|||||||
|
|
||||||
| Component | Implemented responsibility | References |
|
| Component | Implemented responsibility | References |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Root `promptkit` package | Provides the supported engine facade, source, backend-registration, and injection options, public request, result, prompt-inspection, profile-inspection, and ordinary-run observation values, opaque prepared-execution handles, profile construction, extension interfaces, value conversion, redacted formatting, and typed capacity and generation error mapping. | [Package GoDoc](../../doc.go), [observations](../../observations.go), [prepared execution](../../prepared_execution.go), [backend API](../../backends.go), [engine facade](../../engine.go) |
|
| Root `promptkit` package | Provides the supported engine facade, ConfigKit source helpers, collection and resource mapping conveniences, injection options, public requests and results, prompt/profile/requirement inspection and identity inventories, concrete-request validation, explicit smoke diagnostics, observation values, opaque prepared-execution handles, value conversion, redacted formatting, and typed capacity and generation error mapping. | [Package GoDoc](../../doc.go), [observations](../../observations.go), [prepared execution](../../prepared_execution.go), [backend API](../../backends.go), [engine facade](../../engine.go), [smoke diagnostics](../../smoke_diagnostics.go), [identity listing](../../listing.go), [requirement facade](../../requirements.go) |
|
||||||
| `examples/go-library/prepare` | Demonstrates an offline downstream consumer using a prompt file, in-memory profile, inline input, and `Prepare`. It is not a public library package. | [Example program](../../examples/go-library/prepare/main.go) |
|
| `examples/go-library/prepare` | Demonstrates offline preparation from two explicitly selected synthetic collections, source-relative imported content, a scoped view, filtered listings, and operator inventory. It is not a public library package. | [Example program](../../examples/go-library/prepare/main.go), [collection inputs](../../examples/go-library/prepare/collections/support/collection.yaml) |
|
||||||
| `examples/go-library/run` | Demonstrates an offline downstream consumer using a prompt file, in-memory profile, inline input, an injected deterministic model client, and `Run`. It is not a public library package. | [Example program](../../examples/go-library/run/main.go) |
|
| `examples/go-library/run` | Demonstrates an offline downstream consumer using a prompt file, programmatic profile, inline input, an injected deterministic model client, and `Run`. It is not a public library package. | [Example program](../../examples/go-library/run/main.go) |
|
||||||
| `examples/go-library/observe` | Demonstrates offline built-in HTTP retry and synchronous metadata observation across structured proposal and assessment runs using embedded sources, two profiles on one finite backend, and an application decision. It is not a public library package. | [Example program](../../examples/go-library/observe/main.go) |
|
| `examples/go-library/observe` | Demonstrates offline built-in HTTP retry and synchronous metadata observation across structured proposal and assessment runs using embedded sources, two profiles on one finite backend, and an application decision. It is not a public library package. | [Example program](../../examples/go-library/observe/main.go) |
|
||||||
| `internal/backend` | Constructs each engine's immutable registry from maintained definitions and consumer additions, validates and defensively copies definitions through the shared JSON-value package, and consumes the LLM-owned OpenAI-compatible reserved request-field rule. | [Backend registry](../../internal/backend/registry.go) |
|
| `internal/backend` | Holds a defensive, immutable lookup of final backend definitions, including diagnostic metadata, plus capacity policies validated by the configuration adapter. | [Backend registry](../../internal/backend/registry.go) |
|
||||||
| `internal/catalog` | Strictly validates imported immutable maintained backend and profile catalog assets before engine assembly uses them. | [Catalog adapter](../../internal/catalog/catalog.go), [internal sources](sources.md#profiles-and-built-ins) |
|
| `internal/configuration` | Loads root and namespaced collection resources through ConfigKit; records effective field presence, validates final values including exact references and optional diagnostic metadata, captures indexed prompt definitions and source-relative message content, compiled templates, input metadata snapshots, sorted backend/profile/prompt identities, resource namespace sets, effective collection declarations, backend and presence-aware profile snapshots, and the unchanged read-only inspection view. | [Typed loader](../../internal/configuration/load.go), [identity listing](../../internal/configuration/listing.go), [internal sources](sources.md#typed-configuration-loading) |
|
||||||
| `internal/capacity` | Owns engine-local bounded execution admission and FIFO model-generation permits for limited backend IDs, including cancellation-safe waiter removal and client wrapping. | [Internal capacity management](capacity.md) |
|
| `internal/capacity` | Owns engine-local bounded execution admission and FIFO model-generation permits for limited backend IDs, including cancellation-safe waiter removal and client wrapping. | [Internal capacity management](capacity.md) |
|
||||||
| `internal/domain` | Defines internal framework values for requests, artifacts, prompt definitions, profiles, execution targets, rendering, generation, and validation, and owns source-neutral invariants for shared execution settings, OpenAI-compatible base endpoints, session identifiers, and output contracts. Source parsing, required fields, other source-specific normalization, defaulting, and boundary-specific error classification remain with their callers. | [Domain declarations](../../internal/domain/domain.go), [endpoint invariant](../../internal/domain/endpoint.go) |
|
| `internal/domain` | Defines internal framework values for requests, artifacts, prompt definitions, requirement inspection, profiles, execution targets, rendering, generation, validation, and source-neutral smoke diagnostics, and owns source-neutral invariants for shared execution settings, OpenAI-compatible base endpoints, session identifiers, output contracts, and resource-identity syntax and qualification. Source parsing, required fields, other source-specific normalization, defaulting, and boundary-specific error classification remain with their callers. | [Domain declarations](../../internal/domain/domain.go), [requirement values](../../internal/domain/requirements.go), [endpoint invariant](../../internal/domain/endpoint.go) |
|
||||||
| `internal/engine` | Assembles external catalogs, Promptkit-owned profiles, repositories, registry, capacity, collaborators, repairer, and the one use-case runner for each engine instance. | [Engine assembly](../../internal/engine/engine.go), [internal sources](sources.md#profiles-and-built-ins), [internal runner](runner.md) |
|
| `internal/engine` | Assembles caller-selected configuration into one snapshot, then constructs shared capacity, rendering, validation, generation, repair, the ordinary runner, and the smoke tester for each engine. Immutable namespace views share those collaborators and qualify caller-supplied execution, inspection, and diagnostic selections without rewriting captured references. Built-in engines provide the diagnostic HTTP collaborator; injected-client engines do not. It delegates identity listing directly to the captured configuration inventory rather than the runner. | [Engine assembly](../../internal/engine/engine.go), [internal sources](sources.md#typed-configuration-loading), [internal runner](runner.md) |
|
||||||
| `internal/generation` | Coordinates frozen built-in HTTP attempts and one-shot injected calls through active backend permits, including bounded status retries, cancellable backoff, and attempt observations. | [Internal generation coordination](generation.md) |
|
| `internal/generation` | Coordinates frozen built-in HTTP attempts and one-shot injected calls through active backend permits, including bounded status retries, cancellable backoff, ordinary attempt observations, and invocation-local evidence for diagnostics. | [Internal generation coordination](generation.md) |
|
||||||
| `internal/defaults` | Defines application-neutral framework constants and constructs the default execution target. It contains no CLI, server, or inbound HTTP limits. | [Framework defaults](../../internal/defaults/defaults.go) |
|
| `internal/defaults` | Defines application-neutral framework constants and constructs the default execution target. It contains no CLI, server, or inbound HTTP limits. | [Framework defaults](../../internal/defaults/defaults.go) |
|
||||||
| `internal/filecatalog` | Provides deterministic YAML discovery and path helpers for operating-system filesystems and `fs.FS` sources. | [File catalog](../../internal/filecatalog/catalog.go) |
|
| `internal/filecatalog` | Provides deterministic YAML discovery and path helpers for operating-system filesystems and `fs.FS` sources. | [File catalog](../../internal/filecatalog/catalog.go) |
|
||||||
| `internal/jsonvalue` | Validates and deeply copies bounded JSON-compatible extra-parameter and prepared-schema trees while preserving supported concrete value types and rejecting cycles or excessive depth and work. | [JSON values](../../internal/jsonvalue/jsonvalue.go) |
|
| `internal/jsonvalue` | Validates and deeply copies bounded JSON-compatible extra-parameter and prepared-schema trees while preserving supported concrete value types and rejecting cycles or excessive depth and work. | [JSON values](../../internal/jsonvalue/jsonvalue.go) |
|
||||||
| `internal/promptdef` | Loads strictly decoded, validated prompt definitions from filesystem and `fs.FS` sources, including version selection and contained file-backed message content. | [Framework formats](../formats.md), [prompt-definition repository](../../internal/promptdef/filesystem_repository.go) |
|
| `internal/promptdef` | Provides a narrow captured-prompt lookup contract, not-found identity, and execution-ready definition hash. | [Prompt lookup](../../internal/promptdef/repository.go), [framework formats](../formats.md) |
|
||||||
| `internal/profile` | Owns immutable in-memory and filesystem profile repositories, local profile validation, raw-source overlays with error-preserving fallback, and inherited-profile resolution. | [Framework formats](../formats.md), [profile repositories](../../internal/profile/filesystem_repository.go), [internal sources](sources.md#profiles-and-built-ins) |
|
| `internal/profile` | Provides a narrow captured-profile lookup contract and not-found identity. | [Profile lookup](../../internal/profile/repository.go), [framework formats](../formats.md) |
|
||||||
| `internal/prompt` | Renders prompt messages from Go templates with artifact, variable, session, and cache-control data. | [Go-template renderer](../../internal/prompt/go_renderer.go) |
|
| `internal/prompt` | Renders prompt messages from Go templates with artifact, variable, session, and cache-control data, and conservatively enumerates dependencies from captured template syntax. | [Go-template renderer](../../internal/prompt/go_renderer.go), [requirement discovery](../../internal/prompt/requirements.go) |
|
||||||
| `internal/artifact` | Resolves ordinary inline and unrestricted caller-selected file references into copied artifacts with metadata and hashes. | [Internal sources and validation](sources.md) |
|
| `internal/artifact` | Resolves ordinary inline and unrestricted caller-selected file references into copied artifacts with metadata and hashes. | [Internal sources and validation](sources.md) |
|
||||||
| `internal/validate` | Validates basic, JSON, and JSON Schema output using operating-system filesystem or `fs.FS` schema sources and creates operation-local validation plans with canonical contained schema resources. | [Framework formats](../formats.md#schemas), [internal sources and validation](sources.md) |
|
| `internal/validate` | Validates basic, JSON, and JSON Schema output using operating-system filesystem or `fs.FS` schema sources and creates operation-local validation plans with canonical contained schema resources. | [Framework formats](../formats.md#schemas), [internal sources and validation](sources.md) |
|
||||||
| `internal/llm` | Defines the internal generation boundary and implements outbound OpenAI-compatible chat requests from resolved execution targets, including bounded structured non-success response decoding, successful-response decoding, authentication, deadline handling, and ownership of the OpenAI-compatible reserved request-field policy. | [Internal model client](llm.md) |
|
| `internal/llm` | Defines the internal generation boundary and implements outbound OpenAI-compatible chat requests from resolved execution targets, including bounded response decoding, authentication, deadline handling, per-invocation HTTP evidence and built-in diagnostic probes, and ownership of the OpenAI-compatible reserved request-field policy. | [Internal model client](llm.md) |
|
||||||
| `internal/observation` | Owns invocation-local run IDs, presence-aware usage and phase accounting, and synchronous metadata event delivery for ordinary and claimed prepared runs and generation attempts. | [Internal execution observation](observation.md) |
|
| `internal/observation` | Owns invocation-local run IDs, presence-aware usage and phase accounting, and synchronous metadata event delivery for ordinary and claimed prepared runs and generation attempts; smoke diagnostics keep their private evidence outside ordinary observer events. | [Internal execution observation](observation.md) |
|
||||||
| `internal/usecase` | Resolves prompt definitions and hashes, profiles, backends, and targets for exact inspection and request settings for preparation, and coordinates ordinary and single-claim prepared execution across internal sources, rendering, artifact loading, operation-local validation plans, generation, capacity, and bounded repair. | [Internal runner](runner.md), [prepared-execution implementation](../../internal/usecase/prepared_execution.go) |
|
| `internal/usecase` | Selects captured prompts and profiles, enumerates requirements from captured prompt syntax, applies request overrides and completion checks, coordinates ordinary and single-claim prepared execution across rendering, artifact loading, operation-local validation plans, generation, capacity, and bounded repair, and runs explicit smoke diagnostics with private cases, shared generation/capacity, and optional built-in HTTP probes. | [Internal runner](runner.md), [requirement inspection](../../internal/usecase/requirements_inspection.go), [prepared-execution implementation](../../internal/usecase/prepared_execution.go) |
|
||||||
|
|
||||||
The internal engine assembles these internal components without exposing their
|
The internal engine assembles these internal components without exposing their
|
||||||
representations. Consumers depend only on the root facade.
|
representations. Consumers depend only on the root facade.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
This document describes Promptkit's implemented internal orchestration. The
|
This document describes Promptkit's implemented internal orchestration. The
|
||||||
[architecture policy](../policy/architecture.md) owns dependency and consumer
|
[architecture policy](../policy/architecture.md) owns dependency and consumer
|
||||||
boundaries. The [source and validation document](sources.md) owns repository,
|
boundaries. The [source and validation document](sources.md) owns configuration,
|
||||||
artifact, rendering, and validation behavior, while the
|
artifact, rendering, and validation behavior, while the
|
||||||
[model-client document](llm.md) owns generation behavior and failure
|
[model-client document](llm.md) owns generation behavior and failure
|
||||||
categories.
|
categories.
|
||||||
@@ -16,11 +16,10 @@ and override semantics consumed by the runner.
|
|||||||
|
|
||||||
## Collaborators
|
## Collaborators
|
||||||
|
|
||||||
`Runner` coordinates narrow internal interfaces for prompt definitions,
|
`Runner` coordinates narrow internal interfaces for captured prompt definitions
|
||||||
profiles, backend resolution, artifacts, rendering, model generation, and
|
and resolved profiles, artifacts, rendering, model generation, and validation.
|
||||||
validation. The internal engine supplies one immutable registry containing the
|
The internal engine supplies one engine-local run admitter and a generation
|
||||||
built-in backend and validated consumer additions, one engine-local run
|
coordinator using the same capacity manager. Validation
|
||||||
admitter, and a generation coordinator using the same capacity manager. Validation
|
|
||||||
plans and provider-facing schema metadata come from the validator's preparation
|
plans and provider-facing schema metadata come from the validator's preparation
|
||||||
interface.
|
interface.
|
||||||
The internal engine supplies one default output repairer through the explicit
|
The internal engine supplies one default output repairer through the explicit
|
||||||
@@ -33,32 +32,81 @@ The runner has no durable run or session store.
|
|||||||
|
|
||||||
## Shared Prompt Selection
|
## Shared Prompt Selection
|
||||||
|
|
||||||
The runner uses one prompt-selection and hashing boundary for ordinary
|
The internal engine applies each namespace view to slashless caller selections
|
||||||
preparation and exact prompt inspection. Preparation retains its early
|
before they reach the runner. The runner therefore selects captured prompts and
|
||||||
request-ID check before direct-session normalization; both operations then use
|
profiles by the resulting exact runtime IDs. Already-qualified selections pass
|
||||||
the configured prompt repository to select one definition, load referenced
|
through, while configured defaults and stored references are used as authored
|
||||||
message content, and calculate the same prompt hash.
|
and are not requalified.
|
||||||
|
|
||||||
Inspection stops after that structural lookup. It does not parse templates or
|
Ordinary preparation selects the full prompt definition and construction-time
|
||||||
touch profile, artifact, schema, renderer, validator, admission, or model
|
hash. It retains its early request-ID check before direct-session
|
||||||
collaborators. The root [`Engine.InspectPrompt`](../../engine.go) GoDoc owns
|
normalization. Referenced message content and static template syntax were
|
||||||
the public operation's exact contract.
|
captured when the engine was built.
|
||||||
|
|
||||||
|
`InspectPrompt` selects captured metadata through an optional repository
|
||||||
|
capability, falling back to the full definition and existing hash resolution
|
||||||
|
for internal repositories without it. It returns owned metadata without
|
||||||
|
rendering templates or touching profile, artifact, schema, renderer, validator,
|
||||||
|
admission, or model collaborators. The root
|
||||||
|
[`Engine.InspectPrompt`](../../engine.go) GoDoc owns the public operation's
|
||||||
|
exact contract.
|
||||||
|
|
||||||
|
`InspectRequirements` uses the same metadata selection to resolve the
|
||||||
|
request-profile/default identity, then obtains the captured requirement
|
||||||
|
snapshot by the selected prompt ID. The analyzer receives only
|
||||||
|
those identity fields rather than an execution definition. It analyzes the
|
||||||
|
snapshot with the supplied caller variables and normalized direct-session
|
||||||
|
bypass; it does not resolve target readiness or enter the preparation
|
||||||
|
pipeline. Missing snapshot capability is a prompt-load failure rather than a
|
||||||
|
source fallback. The root
|
||||||
|
[`Engine.InspectRequirements`](../../requirements.go) GoDoc owns the public
|
||||||
|
operation's exact contract.
|
||||||
|
|
||||||
## Shared Profile Selection
|
## Shared Profile Selection
|
||||||
|
|
||||||
The runner uses one profile-selection and target-resolution boundary for
|
The runner uses one profile-selection and target-resolution boundary for
|
||||||
ordinary preparation and exact profile inspection. Preparation first selects a
|
ordinary preparation and exact profile inspection. Preparation first selects a
|
||||||
request profile or a prompt default; inspection begins with its required
|
request profile or a prompt default; inspection begins with its required
|
||||||
explicit profile ID. Both then apply the ordinary source precedence, resolve a
|
explicit profile ID. Both use the captured, presence-aware target constructed
|
||||||
named backend, and construct the effective target from framework, backend, and
|
from the final effective profile and backend configuration. Request overrides
|
||||||
profile values.
|
apply afterward; preparation checks endpoint and model completeness only after
|
||||||
|
those overrides. Inspection can report a partial profile without requiring a
|
||||||
Inspection stops after the resulting endpoint and model are structurally
|
runnable target. Neither path checks credential availability or performs prompt,
|
||||||
validated. It does not check credential availability or perform prompt,
|
|
||||||
artifact, schema, rendering, admission, or model-client work. The root
|
artifact, schema, rendering, admission, or model-client work. The root
|
||||||
[`Engine.InspectProfile`](../../engine.go) GoDoc owns the public operation's
|
[`Engine.InspectProfile`](../../engine.go) GoDoc owns the public operation's
|
||||||
exact contract.
|
exact contract.
|
||||||
|
|
||||||
|
## Smoke-Test Diagnostics
|
||||||
|
|
||||||
|
The separate `SmokeTester` in `internal/usecase` selects from the same captured
|
||||||
|
configuration snapshot and backend/profile collaborators as the runner. The
|
||||||
|
internal engine applies namespace-view qualification to slashless caller IDs
|
||||||
|
before diagnostic selection; captured defaults and references remain exact.
|
||||||
|
Explicit profile selection takes precedence over prompt-default selection and a
|
||||||
|
backend diagnostic default; HTTP-only selection does not resolve a default
|
||||||
|
inference profile. Exact public selection rules belong to the root
|
||||||
|
[`Engine.SmokeTest` GoDoc](../../smoke_diagnostics.go).
|
||||||
|
|
||||||
|
Diagnostics do not call `Run`, `Prepare`, or the prompt preparation pipeline.
|
||||||
|
They do not render application messages, load application artifacts, use
|
||||||
|
application schemas, or inspect session templates. Private text and structured
|
||||||
|
cases and the structured case's schema remain internal to the diagnostic
|
||||||
|
operation; they are not entries in prompt/profile listings or inspection.
|
||||||
|
`InspectConfiguration` remains a separate, explicit operator-oriented view of
|
||||||
|
effective settings and source history. A diagnostic report may include selected
|
||||||
|
effective settings and identifiers, so consumers still apply their disclosure
|
||||||
|
policy.
|
||||||
|
|
||||||
|
The engine assembles the smoke tester with the runner's generation coordinator
|
||||||
|
and capacity manager. Inference holds the ordinary logical admission lease and
|
||||||
|
uses the same active permits, retries, timeout, and request evidence path as
|
||||||
|
generation. Reachability and authentication probes bypass generation capacity
|
||||||
|
and do not require a model. An engine with an injected `LLMClient` can run
|
||||||
|
inference diagnostics but has no built-in HTTP probe collaborator; HTTP-only
|
||||||
|
diagnostics report that limitation instead of falling back to another client.
|
||||||
|
The public GoDoc owns the caller-visible timeout, cancellation, report, and
|
||||||
|
transport semantics.
|
||||||
|
|
||||||
## Shared Preparation Pipeline
|
## Shared Preparation Pipeline
|
||||||
|
|
||||||
`Prepare` and `Run` share one private preparation pipeline split at the point
|
`Prepare` and `Run` share one private preparation pipeline split at the point
|
||||||
@@ -66,11 +114,10 @@ where a run can be assigned to its selected backend pool. The resolution phase
|
|||||||
performs only the work needed to validate routing and admission:
|
performs only the work needed to validate routing and admission:
|
||||||
|
|
||||||
1. validate the required prompt selection and normalize any direct session ID;
|
1. validate the required prompt selection and normalize any direct session ID;
|
||||||
2. load the prompt definition and hash the original definition;
|
2. select the captured prompt definition and hash;
|
||||||
3. select the request profile or the prompt's default profile;
|
3. select the request profile or the prompt's default profile;
|
||||||
4. resolve the profile's backend ID, when present;
|
4. retain the profile's selected backend identity, when present;
|
||||||
5. resolve application-neutral defaults, backend defaults, profile values,
|
5. apply explicit request overrides to its captured backend-completed target;
|
||||||
and explicit request overrides in that order;
|
|
||||||
6. validate endpoint, model, numeric overrides, and credential requirements;
|
6. validate endpoint, model, numeric overrides, and credential requirements;
|
||||||
7. resolve and validate the effective output contract without loading its
|
7. resolve and validate the effective output contract without loading its
|
||||||
schema; and
|
schema; and
|
||||||
@@ -95,33 +142,42 @@ one execution-precedence and error-ordering implementation while allowing a
|
|||||||
full backend pool to reject work before expensive schema, artifact, and
|
full backend pool to reject work before expensive schema, artifact, and
|
||||||
rendering operations. `Prepare` discards the plan after returning its public
|
rendering operations. `Prepare` discards the plan after returning its public
|
||||||
metadata. `Run` retains the plan through initial and repaired-output validation
|
metadata. `Run` retains the plan through initial and repaired-output validation
|
||||||
and discards it when the operation ends. Prepared execution stores the same
|
and discards it when the operation ends. Prepared execution transfers the
|
||||||
kind of plan only in its private payload.
|
completed preparation's owned message, input-hash, and copied-schema values
|
||||||
|
into its private payload, while deep-copying mutable provider extras and
|
||||||
|
creating an independent details snapshot. Claim completion or discard clears
|
||||||
|
the execution payload without removing the details snapshot.
|
||||||
|
|
||||||
|
The root facade's `Engine.ValidateRequest` uses the same complete preparation
|
||||||
|
path as `Prepare` and discards the resulting internal prepared value without
|
||||||
|
converting it into a public snapshot. It does not add a runner operation or
|
||||||
|
validator component; request conversion and public error mapping remain at the
|
||||||
|
facade boundary.
|
||||||
|
|
||||||
Pointer-based numeric overrides preserve an explicit zero. Invalid negative or
|
Pointer-based numeric overrides preserve an explicit zero. Invalid negative or
|
||||||
out-of-range values fail as invalid requests. Endpoint overrides do not change
|
out-of-range values fail as invalid requests. Endpoint overrides do not change
|
||||||
the selected backend identity. Non-empty extra-parameter maps replace whole
|
the selected backend identity. Non-empty extra-parameter maps replace whole
|
||||||
lower-precedence maps. A direct API key takes precedence over environment
|
lower-precedence maps. A direct API key takes precedence over environment
|
||||||
lookup; otherwise request, profile, and backend environment-variable names
|
lookup; otherwise request, explicit profile, and backend environment-variable
|
||||||
apply in that order. A profile requiring a direct key clears an inherited
|
names apply in that order. A profile requiring a direct key suppresses an inherited
|
||||||
backend environment name unless the request supplies its own name. Secret
|
backend environment name unless the request supplies its own name. Secret
|
||||||
values remain excluded from serialized metadata.
|
values remain excluded from serialized metadata.
|
||||||
|
|
||||||
Reasoning overrides are tri-state: nil inherits the profile, a pointer to a
|
Reasoning overrides are tri-state: nil inherits the profile, a pointer to a
|
||||||
nonblank string trims and replaces it, and a pointer to a blank string clears
|
nonblank string trims and replaces it, and a pointer to a blank string clears
|
||||||
it. A nonblank direct session is normalized before source loading, bypasses
|
it. A nonblank direct session is normalized before prompt selection, bypasses
|
||||||
the prompt session template, and is applied after ordinary message rendering.
|
the prompt session template, and is applied after ordinary message rendering.
|
||||||
A blank direct value retains prompt-template behavior. The runner clears the
|
A blank direct value retains prompt-template behavior. The runner clears the
|
||||||
template only on a value copy of the definition, so the definition hash always
|
template only on a value copy of the definition, so the definition hash always
|
||||||
describes the original source while the rendered-prompt hash includes the
|
describes captured effective content while the rendered-prompt hash includes the
|
||||||
effective direct or rendered session and the complete effective message
|
effective direct or rendered session and the complete effective message
|
||||||
sequence. The rendered-prompt hash uses a versioned, length-framed SHA-256
|
sequence. The rendered-prompt hash uses a versioned, length-framed SHA-256
|
||||||
encoding of that session, every role and content value, and cache-control
|
encoding of that session, every role and content value, and cache-control
|
||||||
presence and values; its hexadecimal value is opaque.
|
presence and values; its hexadecimal value is opaque.
|
||||||
|
|
||||||
The registry is read-only after engine construction. Concurrent `Prepare` and
|
The configuration snapshot is read-only after engine construction. Concurrent
|
||||||
`Run` calls resolve independent defensive backend values and keep all
|
`Prepare` and `Run` calls copy their resolved targets and keep all invocation
|
||||||
invocation state local.
|
state local.
|
||||||
|
|
||||||
## Run Flow
|
## Run Flow
|
||||||
|
|
||||||
@@ -215,7 +271,7 @@ generation, validation, repair, and cancellation failures.
|
|||||||
Other context cancellation propagates through the invoked collaborator and is
|
Other context cancellation propagates through the invoked collaborator and is
|
||||||
classified by the owning operation. In particular, cancellation observed by
|
classified by the owning operation. In particular, cancellation observed by
|
||||||
validation retains the context identity through the validation error category.
|
validation retains the context identity through the validation error category.
|
||||||
An overlong direct session is an invalid request before source loading, while
|
An overlong direct session is an invalid request before prompt selection, while
|
||||||
an invalid or overlong prompt session template remains a prompt-render failure.
|
an invalid or overlong prompt session template remains a prompt-render failure.
|
||||||
An unknown selected backend, or a selected backend with no configured resolver,
|
An unknown selected backend, or a selected backend with no configured resolver,
|
||||||
is classified as a profile-load failure.
|
is classified as a profile-load failure.
|
||||||
@@ -237,4 +293,8 @@ Changes to orchestration should continue to use the existing package
|
|||||||
interfaces, keep request state local to an invocation, and preserve the shared
|
interfaces, keep request state local to an invocation, and preserve the shared
|
||||||
resolution and completion pipeline. Source, renderer, validator, or
|
resolution and completion pipeline. Source, renderer, validator, or
|
||||||
model-client contract changes belong first in their owning package and
|
model-client contract changes belong first in their owning package and
|
||||||
document.
|
document. The
|
||||||
|
[smoke selection tests](../../internal/usecase/smoke_selection_test.go),
|
||||||
|
[diagnostic orchestration tests](../../internal/usecase/smoke_tester_test.go),
|
||||||
|
and [private inference-case tests](../../internal/usecase/smoke_inference_test.go)
|
||||||
|
own diagnostic selection, report assembly, and inference findings.
|
||||||
|
|||||||
@@ -2,117 +2,163 @@
|
|||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
This document describes Promptkit's implemented internal source, artifact,
|
This document describes Promptkit's implemented configuration, artifact,
|
||||||
rendering, and output-validation behavior. The
|
rendering, and output-validation behavior. The
|
||||||
[architecture policy](../policy/architecture.md) owns the library boundary and
|
[architecture policy](../policy/architecture.md) owns dependency rules, while
|
||||||
dependency rules. None of these internal packages is a supported consumer API,
|
the [framework format reference](../formats.md) owns fields, defaults, and
|
||||||
and the internal engine assembles them behind the public facade's source options and values.
|
source semantics. Internal packages are not consumer APIs.
|
||||||
The [framework format reference](../formats.md) owns the exact file fields,
|
|
||||||
validation modes, built-in catalog, and source precedence.
|
## Typed Configuration Loading
|
||||||
|
|
||||||
|
`internal/configuration` is the single adapter from ConfigKit's final
|
||||||
|
effective configuration to execution-owned definitions. It performs one
|
||||||
|
layered load, captures effective presence (including explicit null), records
|
||||||
|
field-level normalization and derived defaults during resolution, and validates all effective
|
||||||
|
root and collection backend, profile, and prompt values. Replaced lower-layer
|
||||||
|
domain values are not separately validated. It indexes root resources under
|
||||||
|
their unqualified IDs and collection resources under their full
|
||||||
|
`namespace/local-key` IDs while retaining ConfigKit's resolved tree. It builds an immutable backend index and presence-aware profile
|
||||||
|
targets, applying final backend defaults only after profile inheritance. Runtime
|
||||||
|
lookups use those final indexes and exact stored references; they do not rewrite
|
||||||
|
or resolve resources from ConfigKit source history. The runtime/domain packages
|
||||||
|
do not depend on ConfigKit.
|
||||||
|
|
||||||
|
ConfigKit owns source headers, imports, composition, and source evidence.
|
||||||
|
Promptkit does not preparse contributions: it validates the final effective
|
||||||
|
values once, captures the indexed identities and their original resource
|
||||||
|
contexts, and retains ConfigKit's resolved result for explicit operator inspection.
|
||||||
|
Accepted non-main source headers are consumed during composition; the main
|
||||||
|
source declaration remains available in that inspection view. Collection
|
||||||
|
release labels are descriptive metadata, not selection inputs. The
|
||||||
|
[format reference](../formats.md#source-headers-collections-and-resource-ids)
|
||||||
|
owns the exact source-header and collection contracts.
|
||||||
|
|
||||||
|
The `ConfigurationInspection.Diff` facade method compares two retained results
|
||||||
|
through a private wrapper and a narrow `internal/configuration.Diff` bridge.
|
||||||
|
The wrapper keeps `Result[document]` inside the adapter; the bridge delegates
|
||||||
|
once to ConfigKit v1.1.0 and returns its public difference records. Comparison
|
||||||
|
uses only frozen effective values and does not reopen sources, resolve runtime
|
||||||
|
credentials, or enter the execution path. ConfigKit applies sensitivity from
|
||||||
|
both results; reports may still contain ordinary application data. See the
|
||||||
|
[consumer comparison guide](../consumers/pkg-promptkit.md#inspect-the-captured-configuration)
|
||||||
|
and ConfigKit's [v1.1.0 comparison contract](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.1.0/docs/config.md#effective-configuration-diffs).
|
||||||
|
|
||||||
|
After resource materialization and effective-profile snapshot construction, the
|
||||||
|
adapter captures lexically ordered identity inventories for backends, profiles,
|
||||||
|
and prompts, per-kind resource namespace sets, and metadata for effective
|
||||||
|
collection declarations. Listing methods return caller-owned copies from those
|
||||||
|
captured values without consulting ConfigKit or copying resource definitions.
|
||||||
|
|
||||||
|
The root `DirectoryInputs` and `FilesystemInputs` helpers discover sorted YAML
|
||||||
|
files under an explicit root, provide resource context, and leave imports
|
||||||
|
disabled. Each returned slice belongs to one strict-composition group; file
|
||||||
|
order does not confer priority. Engine assembly prepends a dedicated embedded
|
||||||
|
OpenRouter underlay unless disabled, without modifying caller-owned slices.
|
||||||
|
Caller underlays are composed in their given
|
||||||
|
order, followed by the optional main input. The
|
||||||
|
[configuration tests](../../internal/configuration/load_test.go) exercise the
|
||||||
|
real loader and final-value handoff.
|
||||||
|
|
||||||
|
The public `CollectionInput` helper mounts one caller-selected source at
|
||||||
|
`collections/<namespace>` and enables ConfigKit imports for that input. The
|
||||||
|
helper does not read the source; ConfigKit retains responsibility for loading,
|
||||||
|
composition, imports, and source-local resource context. Imported or inherited
|
||||||
|
`content_file` values continue resolving relative to the source that contributed
|
||||||
|
the effective field, not to a flattened runtime identity. Explicit
|
||||||
|
`InspectConfiguration` views expose the retained effective configuration and
|
||||||
|
provenance for operator review; runtime selection and inventories do not derive
|
||||||
|
from source history.
|
||||||
|
|
||||||
## Prompt Definitions
|
## Prompt Definitions
|
||||||
|
|
||||||
`internal/promptdef` uses one source-neutral flow for prompt selection and
|
The configuration adapter indexes each prompt by exact ID,
|
||||||
normalization. That flow scans normalized YAML ID and version metadata,
|
materializes file-backed message content using the exact effective field's
|
||||||
requires one strictly decoded document per file, classifies errors for the
|
origin and resource context, and checks static message/session template syntax
|
||||||
selected definition, detects duplicates, and normalizes the exact match.
|
at construction. It stores captured content, compiled templates, declared
|
||||||
Small operating-system and `fs.FS` adapters own discovery, byte reads, display
|
input metadata, and a definition hash. Alongside the unchanged hashed
|
||||||
paths, content opening, and root containment. Each lookup remains a
|
definition, it retains content-type and description presence for each declared
|
||||||
point-in-time scan: definitions and catalogs are not cached, and file-backed
|
input and provides an internal requirement snapshot with those declarations
|
||||||
message content is opened only for the exact selected candidate.
|
and the captured parses. Metadata values come from the final normalized
|
||||||
|
declaration; absent and null fields remain distinguishable from explicit
|
||||||
|
strings, including empty strings. It closes content-file handles before
|
||||||
|
returning and retains no source capability for public inspection.
|
||||||
|
|
||||||
Message roles are normalized through the shared domain owner by trimming
|
Prompt materialization shares a lazy metadata snapshot across root and collection
|
||||||
Unicode whitespace and lowercasing. Only `developer`, `system`, `user`, and
|
resources during construction, avoiding a full metadata copy per collection.
|
||||||
`assistant` are published; invalid roles remain selected prompt-definition
|
|
||||||
failures rather than becoming request errors. Cache-control metadata uses the
|
|
||||||
same shared domain normalization and defensive-copy rule.
|
|
||||||
|
|
||||||
Operating-system sources enforce containment against canonical roots and
|
Normalization and defaults are recorded at individual resolved field paths,
|
||||||
targets so symlinks cannot escape. Injected `fs.FS` sources enforce containment
|
including message roles and cache controls. List indexes identify fields for
|
||||||
in their clean relative path namespace. A single-file source uses the selected
|
presence, validation, sensitivity, and resource context; full IDs select
|
||||||
prompt file's containing directory as its root. Every content path must be
|
captured runtime indexes. Unchanged content-file fields retain their origins.
|
||||||
relative and is opened from its exact parsed text after a separate blank check;
|
Backend-to-profile and request-time completion remain separate runtime work.
|
||||||
contained parent components and whitespace-bearing names remain valid.
|
|
||||||
|
|
||||||
Exact prompt inspection performs one point-in-time lookup through that same
|
The runner selects full definitions from this index for preparation and uses
|
||||||
repository and validates referenced message content before returning declared
|
captured prompt metadata for `InspectPrompt` and `InspectRequirements`; the
|
||||||
metadata. It does not parse templates or read profile, input, or schema
|
internal requirement snapshot lookup uses the selected prompt ID alone.
|
||||||
sources, and it does not retain the definition for a later execution.
|
These paths do not rescan configuration or reopen message files. `InspectPrompt`
|
||||||
|
returns declared metadata without rendering or resolving profile, input, or
|
||||||
|
schema resources, and its public shape and prompt hashes do not include the
|
||||||
|
captured presence state. Runtime rendering still checks request variables and
|
||||||
|
input helpers. The [prompt adapter tests](../../internal/configuration/prompts_test.go)
|
||||||
|
cover resource origins, context containment, capture, and selection.
|
||||||
|
|
||||||
Its package tests own prompt selection, strict decoding, definition validation,
|
The configuration adapter shares ID-only selection across its internal prompt
|
||||||
duplicate detection, and source containment:
|
lookups. The execution snapshot lookup returns an owned prompt
|
||||||
[prompt-definition repository tests](../../internal/promptdef/repository_test.go).
|
definition and cached hash; metadata inspection returns owned input metadata
|
||||||
|
without copying execution templates. Profile selection likewise has an
|
||||||
|
identity-only lookup for consumers that do not need the execution target,
|
||||||
|
while the full profile lookup continues to return an owned target and deep copy
|
||||||
|
of provider extras. These lookups use captured indexes and do not query
|
||||||
|
ConfigKit again.
|
||||||
|
|
||||||
## Profiles And Built-Ins
|
The `internal/prompt` requirement walker uses the captured parse trees and
|
||||||
|
declaration metadata without executing templates or reading artifacts. It
|
||||||
|
collects literal `input` names, direct root caller keys, and literal-key
|
||||||
|
`index` lookups. For `if` and `with`, it classifies branches when their
|
||||||
|
conditions are simple supported values: direct values, literals, negation, or
|
||||||
|
two-string equality/inequality. It uses Go template truthiness, so a supplied
|
||||||
|
nonempty string such as `false` is true. Missing decision values and
|
||||||
|
content-dependent conditions leave dependency-bearing branches conditional
|
||||||
|
and report incomplete discovery; unresolved conditions with no branch
|
||||||
|
dependencies do not by themselves make discovery incomplete. Recognizable
|
||||||
|
references on known skipped paths remain optional inventory entries with an
|
||||||
|
inactive-use explanation, unless another applicable use establishes a
|
||||||
|
stronger classification. Skipped expressions are scanned only for
|
||||||
|
syntactically recognizable names and are not evaluated. A direct caller value
|
||||||
|
or literal-key lookup can also supply an input name, including through simple
|
||||||
|
parentheses or a direct pipeline to `input`; names are used exactly as
|
||||||
|
supplied. Unresolved names and unsupported dependency-bearing expressions
|
||||||
|
produce structured limitations while retaining recognizable references.
|
||||||
|
Where an unresolved name or scope could affect existing optional entries, those
|
||||||
|
entries are reported as conditional rather than inactive-only or definitively
|
||||||
|
optional. Direct root-map `with` forms retain caller-map identity. Known scalar
|
||||||
|
values remain separate from unknown scopes; accesses through untracked local
|
||||||
|
maps report incomplete discovery rather than silently losing caller keys.
|
||||||
|
Unsupported name expressions reuse value analysis's conservative syntax scan
|
||||||
|
instead of recursively rescanning the same subtree.
|
||||||
|
Invoked named templates are scanned conservatively, and
|
||||||
|
dependency-bearing range scopes make discovery incomplete. Uncalled definitions
|
||||||
|
and bypassed session templates are not traversed. This internal walker does not
|
||||||
|
change rendering behavior.
|
||||||
|
|
||||||
`internal/profile` owns immutable in-memory and filesystem repositories, local
|
## Profiles
|
||||||
validation, overlays, and resolution for execution profiles. In-memory
|
|
||||||
definitions are normalized and validated once at that repository boundary and
|
|
||||||
are defensively copied on storage and lookup. A file contains
|
|
||||||
exactly one YAML document and its trimmed YAML `id` is its only selection
|
|
||||||
identity; filenames do not confer authority. Each point lookup reads discovered
|
|
||||||
files once for their metadata and reuses the selected file's bytes for strict
|
|
||||||
decoding; unrelated profiles are not fully decoded. Strict selected decoding
|
|
||||||
recognizes `base_profile` and the optional `backend` field, trims their values,
|
|
||||||
and permits inherited target fields only when a base is named. File-backed
|
|
||||||
`extra_params` values are validated and defensively copied through the shared
|
|
||||||
bounded JSON-value owner before a profile is published. OpenAI-compatible
|
|
||||||
reserved-field policy remains with the model-client and backend-registry owners.
|
|
||||||
|
|
||||||
`LoadFSRepository` is the eager immutable loading boundary for internal
|
ConfigKit resolves profile inheritance before the handoff. Within a collection,
|
||||||
catalog consumers. It discovers and strictly validates every raw profile once,
|
`extends: {id: parent}` names a sibling local ID; it is not a cross-collection reference.
|
||||||
preserves safe source metadata including explicitly present YAML fields, and
|
The configuration adapter validates final effective resource references and
|
||||||
publishes independently copied values from memory. It does not resolve profile
|
field values, then builds target
|
||||||
inheritance. Configured consumer sources continue to use the lazy point lookup
|
snapshots with backend defaults and presence/suppression state. A profile can
|
||||||
repositories described above.
|
remain partial until runtime overrides complete it. `InspectProfile` selects
|
||||||
|
that captured target without prompt, artifact, schema, credential lookup, or
|
||||||
|
provider work. Preparation applies runtime overrides and then checks target
|
||||||
|
completeness. Identity-only profile selection can identify a partial captured
|
||||||
|
profile without copying its target or requiring readiness. The
|
||||||
|
[target tests](../../internal/configuration/targets_test.go) cover effective
|
||||||
|
target interpretation.
|
||||||
|
|
||||||
`internal/catalog` validates the imported immutable OpenRouter and
|
The internal engine delegates listing through a narrow identity-inventory
|
||||||
Rakestrawhome asset modules as one private adapter boundary. It enforces their
|
interface directly to the configuration snapshot. The execution runner has no
|
||||||
manifest, layout, profile ownership, inheritance, and secret-safety rules
|
listing responsibility.
|
||||||
before returning raw catalog sources. Internal engine assembly uses those validated
|
|
||||||
catalogs as the lowest-precedence profile source.
|
|
||||||
|
|
||||||
Normalized external catalog profile IDs with the exact `promptkit-` prefix are
|
|
||||||
rejected at this adapter boundary. That reservation applies only to imported
|
|
||||||
catalogs; configured consumer sources retain the ordinary profile-ID rules.
|
|
||||||
|
|
||||||
The overlay repository consults the next repository only when the
|
|
||||||
higher-precedence repository reports that a profile is absent. A reliably
|
|
||||||
selected malformed profile stops fallback, while an unrelated malformed file
|
|
||||||
does not become authoritative through its filename. Loading does not check
|
|
||||||
backend registry membership because the available registry belongs to the
|
|
||||||
assembled engine; the runner checks membership during preparation and exact
|
|
||||||
profile inspection.
|
|
||||||
|
|
||||||
The internal engine assembles one raw composite catalog in precedence order:
|
|
||||||
in-memory profiles, one ordinary configured source, an application fallback
|
|
||||||
source, Promptkit-owned profiles, then the maintained external catalog. It
|
|
||||||
constructs the owned definitions through the in-memory repository and validates
|
|
||||||
their maintained-only resolution and backend membership before consumer sources
|
|
||||||
are overlaid. An explicit file or `fs.FS` profile source replaces
|
|
||||||
`Config.ProfileDir` within the ordinary configured-source category. One outer
|
|
||||||
resolving repository wraps that complete raw catalog, so each base lookup
|
|
||||||
observes the same precedence and shadowing rules.
|
|
||||||
|
|
||||||
The resolving repository traverses every selected chain afresh, retains no
|
|
||||||
cache, detects cycles, limits a chain to 32 profiles, merges root-to-leaf into a
|
|
||||||
new caller-owned value, and validates the final target before publishing it. It
|
|
||||||
does not check backend registry membership. Exact `base_profile` syntax, merge
|
|
||||||
rules, and consumer-visible failure behavior belong to the [framework format
|
|
||||||
reference](../formats.md#profile-inheritance).
|
|
||||||
|
|
||||||
Exact profile inspection performs one point-in-time resolved lookup through
|
|
||||||
those profile sources and checks the final target without reading prompt, input,
|
|
||||||
or schema sources. It does not retain that lookup for a later execution.
|
|
||||||
Prepared execution instead freezes the fully resolved target; a later ordinary
|
|
||||||
operation performs a fresh traversal.
|
|
||||||
|
|
||||||
The imported external catalog provides external profiles and matching backend
|
|
||||||
definitions. The internal engine owns the Promptkit profiles documented in the
|
|
||||||
[framework format reference](../formats.md#built-in-profile-catalog). Profile
|
|
||||||
loading and overlay behavior are owned by the [profile repository tests](../../internal/profile/repository_test.go), while
|
|
||||||
catalog completeness, backend selection, and duplicate IDs are owned by the
|
|
||||||
[catalog adapter tests](../../internal/catalog/catalog_test.go).
|
|
||||||
|
|
||||||
## Ordinary Artifacts
|
## Ordinary Artifacts
|
||||||
|
|
||||||
@@ -123,10 +169,15 @@ and after opening the file. It copies content into an artifact, records
|
|||||||
metadata and an opaque content-equality value, and applies a content-type
|
metadata and an opaque content-equality value, and applies a content-type
|
||||||
fallback.
|
fallback.
|
||||||
|
|
||||||
Regular files are read synchronously in bounded chunks. Cancellation is
|
Regular files are read synchronously in bounded chunks. The opened file's size
|
||||||
checked before opening, before and after every read, and before publishing the
|
is only an initial-capacity hint, capped at 1 MiB; reads continue to EOF, and
|
||||||
artifact, so a canceled read never publishes partial content. The ordinary
|
the returned body determines artifact size and hash. If an overreported hint
|
||||||
reader does not detach file reads into background goroutines.
|
leaves unused capacity greater than the actual body length, the completed body
|
||||||
|
is copied to release the excess backing storage, including when the body is
|
||||||
|
empty. Cancellation is checked before opening, before and after every read,
|
||||||
|
and before publishing the artifact, so a
|
||||||
|
canceled read never publishes partial content. The ordinary reader does not
|
||||||
|
detach file reads into background goroutines.
|
||||||
|
|
||||||
This ordinary reader does not implement an inbound HTTP security boundary. In
|
This ordinary reader does not implement an inbound HTTP security boundary. In
|
||||||
particular, it does not constrain files to an application root or impose an
|
particular, it does not constrain files to an application root or impose an
|
||||||
@@ -137,19 +188,22 @@ implemented reader behavior and failures.
|
|||||||
|
|
||||||
## Rendering
|
## Rendering
|
||||||
|
|
||||||
`internal/prompt` renders definition messages as Go templates using named
|
`internal/prompt` renders captured, pre-parsed definition templates using named
|
||||||
artifacts and variables. Within one render, each referenced artifact body is
|
artifacts and variables. It clones templates to bind request-specific input
|
||||||
converted to text lazily and cached by input name for reuse across the session
|
helpers without mutating shared state. Within one render, each referenced
|
||||||
and every message; the cache is not shared across renders. Conversion uses
|
artifact body is converted to text lazily and cached by input name for reuse
|
||||||
bounded chunks and preserves the artifact bytes exactly.
|
across the session and every message; the cache is not shared across renders.
|
||||||
|
Conversion uses bounded chunks and preserves the artifact bytes exactly.
|
||||||
|
|
||||||
Session and message parsing and execution remain synchronous. The renderer
|
Template parsing occurs during engine construction. Runtime execution remains
|
||||||
checks cancellation before and after each parse and execution boundary,
|
synchronous. The renderer checks cancellation around execution boundaries,
|
||||||
between artifact conversion chunks, around each message, and before publishing
|
between artifact conversion chunks, around each message, and before publishing
|
||||||
the complete prompt. It cannot interrupt template work already in progress and
|
the complete prompt. It cannot interrupt template work already in progress and
|
||||||
never publishes a partial prompt after observing cancellation. It validates and
|
never publishes a partial prompt after observing cancellation. It validates and
|
||||||
canonicalizes message roles before carrying roles, session IDs, and cache
|
canonicalizes message roles before carrying roles, session IDs, and cache
|
||||||
control into the rendered prompt. The
|
control into the rendered prompt. Message and session templates execute into
|
||||||
|
per-call string builders, so their rendered strings do not require a final
|
||||||
|
buffer-to-string copy. The
|
||||||
[renderer tests](../../internal/prompt/renderer_test.go) own rendering behavior.
|
[renderer tests](../../internal/prompt/renderer_test.go) own rendering behavior.
|
||||||
|
|
||||||
## Schemas And Output Validation
|
## Schemas And Output Validation
|
||||||
@@ -157,7 +211,8 @@ control into the rendered prompt. The
|
|||||||
`internal/validate` provides validators backed by an operating-system
|
`internal/validate` provides validators backed by an operating-system
|
||||||
filesystem or an `fs.FS`. Invalid generated content is returned as a validation
|
filesystem or an `fs.FS`. Invalid generated content is returned as a validation
|
||||||
result; inability to load, register, or compile a schema is an operational
|
result; inability to load, register, or compile a schema is an operational
|
||||||
error.
|
error. Basic validation checks body emptiness on the byte slice directly
|
||||||
|
instead of first making a response-sized string copy.
|
||||||
|
|
||||||
Every preparation operation creates one operation-local validation plan. None,
|
Every preparation operation creates one operation-local validation plan. None,
|
||||||
basic, and JSON modes retain the effective output contract without source
|
basic, and JSON modes retain the effective output contract without source
|
||||||
@@ -181,9 +236,10 @@ not detach dependency work into background goroutines.
|
|||||||
`Prepare` discards its validation plan after returning metadata. `Run` retains
|
`Prepare` discards its validation plan after returning metadata. `Run` retains
|
||||||
its plan for initial and repaired-output validation, then discards it with the
|
its plan for initial and repaired-output validation, then discards it with the
|
||||||
operation. `PrepareExecution` retains the plan in its private frozen payload;
|
operation. `PrepareExecution` retains the plan in its private frozen payload;
|
||||||
`RunPrepared` uses that plan without reopening prompt, profile, input, or
|
`RunPrepared` uses that plan without reopening configuration, message, input,
|
||||||
schema sources or rerendering the request. A later ordinary `Run` always
|
or schema resources or rerendering the request. A later ordinary `Run` uses
|
||||||
performs fresh source resolution and preparation.
|
the same captured configuration but prepares request artifacts and schemas
|
||||||
|
afresh.
|
||||||
|
|
||||||
The [validator tests](../../internal/validate/standard_validator_test.go) own
|
The [validator tests](../../internal/validate/standard_validator_test.go) own
|
||||||
basic, JSON, JSON Schema, source resolution, schema loading, compilation,
|
basic, JSON, JSON Schema, source resolution, schema loading, compilation,
|
||||||
|
|||||||
118
docs/oss/attribution.md
Normal file
118
docs/oss/attribution.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# Open Source Attribution Notices
|
||||||
|
|
||||||
|
## Standard Repository Policies
|
||||||
|
|
||||||
|
Material: policy documents under `docs/policy/` and the Policy Reading Guide in
|
||||||
|
`docs/development.md`, adapted for this repository; root `AGENTS.md`, copied
|
||||||
|
without modification from upstream `agents/AGENTS.md`.
|
||||||
|
|
||||||
|
The release policy uses upstream commit
|
||||||
|
`e833faddf120c789a9b45348edaf75720de16348`; the remaining material uses
|
||||||
|
`d84a38ebab26f048cafbe59a2246700c35d29843`.
|
||||||
|
|
||||||
|
Copyright (C) 2026 Eric Rakestraw
|
||||||
|
|
||||||
|
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
License and notices: [CC BY-SA 4.0](licenses/CC-BY-SA-4.0.txt) |
|
||||||
|
[COPYRIGHT](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/COPYRIGHT) |
|
||||||
|
[LICENSE](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/LICENSE) |
|
||||||
|
[COPYRIGHT](https://gitea.maximumdirect.net/eric/policy/src/commit/e833faddf120c789a9b45348edaf75720de16348/COPYRIGHT) |
|
||||||
|
[LICENSE](https://gitea.maximumdirect.net/eric/policy/src/commit/e833faddf120c789a9b45348edaf75720de16348/LICENSE).
|
||||||
|
|
||||||
|
[Source](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843).
|
||||||
|
|
||||||
|
[Source](https://gitea.maximumdirect.net/eric/policy/src/commit/e833faddf120c789a9b45348edaf75720de16348).
|
||||||
|
|
||||||
|
## ConfigKit
|
||||||
|
|
||||||
|
Selected module: `go.maximumdirect.net/configkit` at `v1.1.0`.
|
||||||
|
|
||||||
|
Copyright (C) 2026 Eric Rakestraw
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
License and notices: [LGPL 3.0](../../LICENSE) |
|
||||||
|
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/configkit/src/commit/191015e289d7f2b91abb9fdd9c4f2f5cf75b4de9/COPYRIGHT) |
|
||||||
|
[LICENSE](https://forge.maximumdirect.net/base-layer/configkit/src/commit/191015e289d7f2b91abb9fdd9c4f2f5cf75b4de9/LICENSE).
|
||||||
|
|
||||||
|
[Source](https://forge.maximumdirect.net/base-layer/configkit/src/commit/191015e289d7f2b91abb9fdd9c4f2f5cf75b4de9).
|
||||||
|
|
||||||
|
## ReleaseKit
|
||||||
|
|
||||||
|
Selected module: `go.maximumdirect.net/releasekit` at `v0.9.0` (maintainer tool).
|
||||||
|
|
||||||
|
Copyright (C) 2026 Eric Rakestraw
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
License and notices: [LGPL 3.0](../../LICENSE) |
|
||||||
|
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/releasekit/src/commit/2180aa868ec2caa95fc8279fd59d5f0ebac41bd4/COPYRIGHT) |
|
||||||
|
[LICENSE](https://forge.maximumdirect.net/base-layer/releasekit/src/commit/2180aa868ec2caa95fc8279fd59d5f0ebac41bd4/LICENSE).
|
||||||
|
|
||||||
|
[Source](https://forge.maximumdirect.net/base-layer/releasekit/src/commit/2180aa868ec2caa95fc8279fd59d5f0ebac41bd4).
|
||||||
|
|
||||||
|
## jsonschema
|
||||||
|
|
||||||
|
Selected module: `github.com/santhosh-tekuri/jsonschema/v6` at `v6.0.2`.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
License and notices: [Apache 2.0](licenses/Apache-2.0.txt) |
|
||||||
|
[LICENSE](https://github.com/santhosh-tekuri/jsonschema/blob/29cbed948d24a04700eb94436416b18a07953b71/LICENSE).
|
||||||
|
|
||||||
|
[Source](https://github.com/santhosh-tekuri/jsonschema/tree/29cbed948d24a04700eb94436416b18a07953b71).
|
||||||
|
|
||||||
|
## Go Text
|
||||||
|
|
||||||
|
Selected module: `golang.org/x/text` at `v0.14.0` (transitive dependency).
|
||||||
|
|
||||||
|
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
License and notices: [BSD 3-Clause](licenses/BSD-3-Clause.txt) |
|
||||||
|
[LICENSE](https://github.com/golang/text/blob/6c97a165dd661335ff7bce6104a008558123c353/LICENSE) |
|
||||||
|
[PATENTS](https://github.com/golang/text/blob/6c97a165dd661335ff7bce6104a008558123c353/PATENTS).
|
||||||
|
|
||||||
|
[Source](https://github.com/golang/text/tree/6c97a165dd661335ff7bce6104a008558123c353).
|
||||||
|
|
||||||
|
## Go YAML v3
|
||||||
|
|
||||||
|
Selected module: `gopkg.in/yaml.v3` at `v3.0.1` (transitive dependency).
|
||||||
|
|
||||||
|
Copyright (c) 2006-2010 Kirill Simonov
|
||||||
|
|
||||||
|
Copyright (c) 2006-2011 Kirill Simonov
|
||||||
|
|
||||||
|
Copyright (c) 2011-2019 Canonical Ltd
|
||||||
|
|
||||||
|
Copyright 2011-2016 Canonical Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: MIT AND Apache-2.0
|
||||||
|
|
||||||
|
License and notices: [MIT](licenses/MIT.txt) |
|
||||||
|
[Apache 2.0](licenses/Apache-2.0.txt) |
|
||||||
|
[LICENSE](https://github.com/go-yaml/yaml/blob/f6f7691b1fdeb513f56608cd2c32c51f8194bf51/LICENSE) |
|
||||||
|
[NOTICE](https://github.com/go-yaml/yaml/blob/f6f7691b1fdeb513f56608cd2c32c51f8194bf51/NOTICE).
|
||||||
|
|
||||||
|
[Source](https://github.com/go-yaml/yaml/tree/f6f7691b1fdeb513f56608cd2c32c51f8194bf51).
|
||||||
|
|
||||||
|
## Go YAML v4
|
||||||
|
|
||||||
|
Selected module: `go.yaml.in/yaml/v4` at `v4.0.0-rc.6` (transitive dependency).
|
||||||
|
|
||||||
|
Copyright 2006-2010 Kirill Simonov
|
||||||
|
|
||||||
|
Copyright 2011-2019 Canonical Ltd
|
||||||
|
|
||||||
|
Copyright 2025 The go-yaml Project Contributors
|
||||||
|
|
||||||
|
SPDX-License-Identifier: MIT AND Apache-2.0
|
||||||
|
|
||||||
|
License and notices: [MIT](licenses/MIT.txt) |
|
||||||
|
[Apache 2.0](licenses/Apache-2.0.txt) |
|
||||||
|
[LICENSE](https://github.com/yaml/go-yaml/blob/1c17b9cee72bf81ae73a8be50fbd7dd3a9985125/LICENSE) |
|
||||||
|
[NOTICE](https://github.com/yaml/go-yaml/blob/1c17b9cee72bf81ae73a8be50fbd7dd3a9985125/NOTICE).
|
||||||
|
|
||||||
|
[Source](https://github.com/yaml/go-yaml/tree/1c17b9cee72bf81ae73a8be50fbd7dd3a9985125).
|
||||||
202
docs/oss/licenses/Apache-2.0.txt
Normal file
202
docs/oss/licenses/Apache-2.0.txt
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
11
docs/oss/licenses/BSD-3-Clause.txt
Normal file
11
docs/oss/licenses/BSD-3-Clause.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Copyright (c) <year> <owner>.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
170
docs/oss/licenses/CC-BY-SA-4.0.txt
Normal file
170
docs/oss/licenses/CC-BY-SA-4.0.txt
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
Creative Commons Attribution-ShareAlike 4.0 International
|
||||||
|
|
||||||
|
Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.
|
||||||
|
|
||||||
|
Using Creative Commons Public Licenses
|
||||||
|
|
||||||
|
Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.
|
||||||
|
|
||||||
|
Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors.
|
||||||
|
|
||||||
|
Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described.
|
||||||
|
|
||||||
|
Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public.
|
||||||
|
|
||||||
|
Creative Commons Attribution-ShareAlike 4.0 International Public License
|
||||||
|
|
||||||
|
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
|
||||||
|
|
||||||
|
Section 1 – Definitions.
|
||||||
|
|
||||||
|
a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
|
||||||
|
|
||||||
|
b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License.
|
||||||
|
|
||||||
|
d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
|
||||||
|
|
||||||
|
e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
|
||||||
|
|
||||||
|
f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
|
||||||
|
|
||||||
|
g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike.
|
||||||
|
|
||||||
|
h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
|
||||||
|
|
||||||
|
i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
|
||||||
|
|
||||||
|
j. Licensor means the individual(s) or entity(ies) granting rights under this Public License.
|
||||||
|
|
||||||
|
k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
|
||||||
|
|
||||||
|
l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
|
||||||
|
|
||||||
|
m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
|
||||||
|
|
||||||
|
Section 2 – Scope.
|
||||||
|
|
||||||
|
a. License grant.
|
||||||
|
|
||||||
|
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
|
||||||
|
|
||||||
|
A. reproduce and Share the Licensed Material, in whole or in part; and
|
||||||
|
|
||||||
|
B. produce, reproduce, and Share Adapted Material.
|
||||||
|
|
||||||
|
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
|
||||||
|
|
||||||
|
3. Term. The term of this Public License is specified in Section 6(a).
|
||||||
|
|
||||||
|
4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
|
||||||
|
|
||||||
|
5. Downstream recipients.
|
||||||
|
|
||||||
|
A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
B. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply.
|
||||||
|
|
||||||
|
C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
|
||||||
|
|
||||||
|
6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
|
||||||
|
|
||||||
|
b. Other rights.
|
||||||
|
|
||||||
|
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
|
||||||
|
|
||||||
|
2. Patent and trademark rights are not licensed under this Public License.
|
||||||
|
|
||||||
|
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties.
|
||||||
|
|
||||||
|
Section 3 – License Conditions.
|
||||||
|
|
||||||
|
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
|
||||||
|
|
||||||
|
a. Attribution.
|
||||||
|
|
||||||
|
1. If You Share the Licensed Material (including in modified form), You must:
|
||||||
|
|
||||||
|
A. retain the following if it is supplied by the Licensor with the Licensed Material:
|
||||||
|
|
||||||
|
i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
|
||||||
|
|
||||||
|
ii. a copyright notice;
|
||||||
|
|
||||||
|
iii. a notice that refers to this Public License;
|
||||||
|
|
||||||
|
iv. a notice that refers to the disclaimer of warranties;
|
||||||
|
|
||||||
|
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
|
||||||
|
|
||||||
|
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
|
||||||
|
|
||||||
|
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
|
||||||
|
|
||||||
|
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
|
||||||
|
|
||||||
|
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
|
||||||
|
|
||||||
|
b. ShareAlike.In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
|
||||||
|
|
||||||
|
1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License.
|
||||||
|
|
||||||
|
2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
|
||||||
|
|
||||||
|
3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
|
||||||
|
|
||||||
|
Section 4 – Sui Generis Database Rights.
|
||||||
|
|
||||||
|
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
|
||||||
|
|
||||||
|
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database;
|
||||||
|
|
||||||
|
b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
|
||||||
|
|
||||||
|
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
|
||||||
|
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
|
||||||
|
|
||||||
|
Section 5 – Disclaimer of Warranties and Limitation of Liability.
|
||||||
|
|
||||||
|
a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
|
||||||
|
|
||||||
|
b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
|
||||||
|
|
||||||
|
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
|
||||||
|
|
||||||
|
Section 6 – Term and Termination.
|
||||||
|
|
||||||
|
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
|
||||||
|
|
||||||
|
b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
|
||||||
|
|
||||||
|
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
|
||||||
|
|
||||||
|
2. upon express reinstatement by the Licensor.
|
||||||
|
|
||||||
|
c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
|
||||||
|
|
||||||
|
d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
|
||||||
|
|
||||||
|
e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
|
||||||
|
|
||||||
|
Section 7 – Other Terms and Conditions.
|
||||||
|
|
||||||
|
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
|
||||||
|
|
||||||
|
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
Section 8 – Interpretation.
|
||||||
|
|
||||||
|
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
|
||||||
|
|
||||||
|
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
|
||||||
|
|
||||||
|
c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
|
||||||
|
|
||||||
|
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
|
||||||
|
|
||||||
|
Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
|
||||||
|
|
||||||
|
Creative Commons may be contacted at creativecommons.org.
|
||||||
18
docs/oss/licenses/MIT.txt
Normal file
18
docs/oss/licenses/MIT.txt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) <year> <copyright holders>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
# Agent Policy
|
# Agent Policy
|
||||||
|
|
||||||
**Base Policy Version:** 1.1
|
**Base Policy Version:** 1.3
|
||||||
|
|
||||||
**Last Updated:** 2026-09-23
|
**Last Updated**: 2026-09-27
|
||||||
|
|
||||||
|
**Upstream Source:** [agents.md](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/agents.md)
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
Agents must follow these house rules before recommending, planning, or
|
Agents must read and follow these rules.
|
||||||
implementing repository changes.
|
|
||||||
|
|
||||||
## Implementation Discipline
|
## Implementation Discipline
|
||||||
|
|
||||||
Write the simplest idiomatic code that satisfies the user's request.
|
**KEY PRINCIPLES:** Write the simplest idiomatic code that satisfies the user's
|
||||||
Simplify the mechanism, not the user's choices. Do not implicitly strengthen
|
request. Do not implicitly strengthen requirements, restrictions, or guarantees
|
||||||
requirements, restrictions, or guarantees.
|
beyond what the user actually requested.
|
||||||
|
|
||||||
**Keep implementation machinery proportionate.** Use straightforward,
|
**Keep implementation machinery proportionate.** Use straightforward,
|
||||||
idiomatic code and introduce only the machinery needed to support the requested
|
idiomatic code and introduce only the machinery needed to support the requested
|
||||||
@@ -28,41 +29,48 @@ operator and caller choice where policy has not been established. If an
|
|||||||
additional restriction appears advisable, present it as a decision rather than
|
additional restriction appears advisable, present it as a decision rather than
|
||||||
silently implementing it.
|
silently implementing it.
|
||||||
|
|
||||||
|
**Do not invent guarantees.** Do not strengthen public contracts, guarantees,
|
||||||
|
or invariants beyond those expressly requested by the user.
|
||||||
|
|
||||||
|
**Use the standard library.** For example: in almost every case, the best
|
||||||
|
approach to copying files is to use the standard library. Writing custom code to
|
||||||
|
implement power-loss protection at the application level, or to guard against
|
||||||
|
bad actors with privileged and concurrent access to the host's filesystem,
|
||||||
|
is wasteful, unnecessary, and not idiomatic. Stick to the simple, standard, and
|
||||||
|
idiomatic approach unless another path is expressly requested by the user.
|
||||||
|
|
||||||
|
**An implementation detail is not repository policy.** Tests and documentation
|
||||||
|
must not elevate incidental behavior into a durable guarantee or restriction
|
||||||
|
unless that behavior was expressly selected as part of the contract.
|
||||||
|
|
||||||
|
## Promptkit Contract Authority
|
||||||
|
|
||||||
|
The following clarification preserves Promptkit's established distinction
|
||||||
|
between implementation choices and authorized contracts.
|
||||||
|
|
||||||
A correctness constraint must follow from the requested behavior or an
|
A correctness constraint must follow from the requested behavior or an
|
||||||
established invariant, not from the convenience of the chosen implementation.
|
established invariant, not from the convenience of the chosen implementation.
|
||||||
Explain the specific failure it prevents and why a less restrictive
|
Explain the specific failure it prevents and why a less restrictive
|
||||||
implementation cannot satisfy the contract.
|
implementation cannot satisfy the contract.
|
||||||
|
|
||||||
**An implementation detail is not repository policy.** Tests and documentation
|
Existing explicit contracts remain authoritative; changing or weakening one
|
||||||
must not elevate incidental behavior into a durable guarantee or restriction
|
requires a deliberate decision. Agent-authored plans, tests, documentation,
|
||||||
unless that behavior was expressly selected as part of the contract. Existing
|
and prior implementations are not evidence that an additional restriction was
|
||||||
explicit contracts remain authoritative; changing or weakening one requires a
|
deliberately approved. Identify the authorizing requirement or decision; when
|
||||||
deliberate decision rather than being inferred from this principle.
|
that authority is unclear, surface the ambiguity rather than treating repetition
|
||||||
|
as approval.
|
||||||
Agent-authored plans, tests, documentation, and prior implementations are not
|
|
||||||
evidence that an additional restriction was deliberately approved. Identify the
|
|
||||||
authorizing requirement or decision; when that authority is unclear, surface
|
|
||||||
the ambiguity rather than treating repetition as approval.
|
|
||||||
|
|
||||||
**Do not invent guarantees.** Do not strengthen public contracts, guarantees,
|
|
||||||
or invariants beyond those expressly requested by the user.
|
|
||||||
|
|
||||||
**Use the standard library.** Prefer simple, idiomatic standard-library
|
|
||||||
implementations when they satisfy the requested behavior. Do not add custom
|
|
||||||
power-loss protection or defenses against privileged concurrent filesystem
|
|
||||||
actors unless expressly required. Preserve existing repository contracts.
|
|
||||||
|
|
||||||
## Git Commit Policy
|
## Git Commit Policy
|
||||||
|
|
||||||
By default, create a commit after completing each coherent unit of work, using a
|
By default, commit each completed, nonempty unit of work using a
|
||||||
concise, plain-English message describing what changed. This policy is standing
|
concise, plain-English message describing what changed. This policy is standing
|
||||||
authorization to create those local commits; a separate user request is not
|
authorization to create those local commits; a separate user request is not
|
||||||
required. An explicit user instruction to leave work uncommitted takes precedence.
|
required. An explicit user instruction to leave work uncommitted takes precedence.
|
||||||
|
|
||||||
A unit of work is a focused, reviewable outcome, including its relevant tests and
|
A unit of work is a focused, reviewable outcome, including its relevant tests and
|
||||||
documentation. Do not commit after every intermediate edit or create empty commits.
|
documentation.
|
||||||
Complete the appropriate validation before committing. If work is incomplete or
|
Complete the appropriate validation before committing. If work is incomplete or
|
||||||
validation is blocked, report that state rather than presenting it as completed.
|
validation is blocked, report that state accurately.
|
||||||
|
|
||||||
Before committing:
|
Before committing:
|
||||||
|
|
||||||
@@ -70,10 +78,10 @@ Before committing:
|
|||||||
changes. Identify changes that predate the current unit of work.
|
changes. Identify changes that predate the current unit of work.
|
||||||
2. Exclude secrets, local environment files, generated junk, and unrelated changes.
|
2. Exclude secrets, local environment files, generated junk, and unrelated changes.
|
||||||
3. Stage only the files or hunks belonging to the completed unit. Use `git add .`
|
3. Stage only the files or hunks belonging to the completed unit. Use `git add .`
|
||||||
only when every included change belongs to that unit. Do not disturb unrelated
|
only when every included change belongs to that unit. Preserve unrelated
|
||||||
staged or unstaged work.
|
staged and unstaged work.
|
||||||
4. Review `git diff --staged` and run appropriate checks for the change. Do not
|
4. Review `git diff --staged` and run appropriate checks for the change. Reuse
|
||||||
repeat checks already passed for unchanged content merely to create a commit.
|
successful checks for unchanged content.
|
||||||
5. Review `git log --oneline -n 10` and use
|
5. Review `git log --oneline -n 10` and use
|
||||||
`git commit -m "<concise plain-English commit message>"`.
|
`git commit -m "<concise plain-English commit message>"`.
|
||||||
6. Report the commit identifier and any remaining uncommitted work.
|
6. Report the commit identifier and any remaining uncommitted work.
|
||||||
|
|||||||
@@ -1,15 +1,41 @@
|
|||||||
# Architecture Policy
|
# Architecture Policy
|
||||||
|
|
||||||
**Base Policy Version:** 1.0
|
**Base Policy Version:** 1.3
|
||||||
|
|
||||||
**Last Updated:** 2026-09-23
|
**Last Updated**: 2026-09-27
|
||||||
|
|
||||||
## Purpose
|
**Upstream Source:** [architecture.md](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/architecture.md)
|
||||||
|
|
||||||
This document defines Promptkit's current high-level architecture and the
|
## Purpose And Ownership
|
||||||
durable boundaries that implementation changes must preserve. The
|
|
||||||
[internal component overview](../internal/overview.md) inventories concrete
|
The adopted policy defines Promptkit's purpose and the architectural
|
||||||
implemented packages without redefining these rules.
|
constraints contributors must follow. It owns system boundaries, responsibility
|
||||||
|
and dependency rules, established invariants, and explicit non-goals.
|
||||||
|
|
||||||
|
Describe the concrete package inventory and implementation mechanics in internal
|
||||||
|
documentation. Link to canonical public contracts for their exact behavior and
|
||||||
|
to ADRs for decision rationale.
|
||||||
|
|
||||||
|
## Architectural Approach
|
||||||
|
|
||||||
|
Hexagonal architecture is strongly recommended, including encapsulating external
|
||||||
|
integrations and APIs within internal adapters. Keep domain and application
|
||||||
|
logic independent of external integration details, and make dependency direction
|
||||||
|
clear in the adopted policy.
|
||||||
|
|
||||||
|
Implement that separation with proportionate, idiomatic code. Choose interfaces
|
||||||
|
and package layout to serve the application's actual boundaries. Use adapters
|
||||||
|
for collaborators with external integration responsibilities.
|
||||||
|
|
||||||
|
## Adopted Constraints And Implemented Structure
|
||||||
|
|
||||||
|
Architectural constraints govern current and future development. Record
|
||||||
|
implemented structure accurately, proposals and implementation status in the
|
||||||
|
roadmap, and significant decisions and rationale in ADRs. Describe a target
|
||||||
|
structure as implemented when its changes land.
|
||||||
|
|
||||||
|
Constraints must reflect deliberately selected requirements and decisions.
|
||||||
|
Do not promote incidental implementation choices into architectural invariants.
|
||||||
|
|
||||||
## System Shape
|
## System Shape
|
||||||
|
|
||||||
@@ -25,23 +51,22 @@ The [internal component overview](../internal/overview.md) owns the inventory
|
|||||||
of implemented packages and their concrete responsibilities. This policy owns
|
of implemented packages and their concrete responsibilities. This policy owns
|
||||||
the boundaries and dependency rules those components must preserve.
|
the boundaries and dependency rules those components must preserve.
|
||||||
|
|
||||||
The internal engine assembles one immutable backend registry, one capacity manager,
|
The internal engine composes caller-selected ConfigKit inputs once, then
|
||||||
the internal repositories, renderer, validator, outbound client, and use-case
|
assembles the captured definitions, capacity manager, renderer, validator,
|
||||||
runner. The registry contains validated maintained definitions plus engine-scoped
|
outbound client, and use-case runner. The configuration adapter interprets and
|
||||||
consumer additions. The root facade translates public values and errors at the
|
validates only the final effective values, including presence, null, and
|
||||||
library boundary.
|
resource context. It retains source history for explicit operator inspection,
|
||||||
The internal engine constructs the capacity manager from the registry's immutable
|
not for validation or a second resolution pass. The root facade translates
|
||||||
policy snapshot, wraps the selected built-in or injected model client, and
|
public values and errors at the library boundary.
|
||||||
supplies bounded admission to the runner. The defaults and renderer depend on
|
|
||||||
the domain model. Prompt-definition and profile repositories use the domain
|
The engine constructs capacity policy from final backend definitions, wraps
|
||||||
model, file catalog, and YAML decoder. The catalog adapter validates imported
|
the selected built-in or injected model client, and supplies bounded admission
|
||||||
immutable maintained data, including its reserved namespace, before internal
|
to the runner. Runtime/domain components consume execution-owned values, not
|
||||||
engine assembly supplies it to the profile package. Artifact reading uses the
|
ConfigKit types. Artifact reading and validation use the domain model and
|
||||||
domain model and application-neutral defaults. Validation uses the domain model,
|
application-neutral defaults; validation also uses file-catalog helpers and
|
||||||
file catalog, and JSON Schema implementation. The model client uses the domain
|
JSON Schema. The model client uses the domain model, defaults, and an injected
|
||||||
model and application-neutral defaults, and an injected or standard-library HTTP
|
or standard-library HTTP client. The runner coordinates narrow lookup and
|
||||||
client. The use-case runner depends on the narrow interfaces owned by each
|
collaborator interfaces without rereading configuration.
|
||||||
internal component, including backend lookup and run admission.
|
|
||||||
|
|
||||||
The current implementation follows this dependency direction:
|
The current implementation follows this dependency direction:
|
||||||
|
|
||||||
@@ -58,9 +83,10 @@ downstream consumers, including Scriptorium
|
|||||||
narrow injected abstractions
|
narrow injected abstractions
|
||||||
```
|
```
|
||||||
|
|
||||||
The backend registry depends on the domain model and shared JSON-value
|
The backend index depends on the domain model and shared JSON-value ownership
|
||||||
validation, has no mutation API after construction, and consumes the
|
and has no mutation API after construction. The configuration adapter applies
|
||||||
OpenAI-compatible reserved request-field rule owned by the model client. The
|
the OpenAI-compatible reserved request-field rule owned by the model client
|
||||||
|
when it validates final provider maps. The
|
||||||
capacity component depends on the domain model and the narrow internal
|
capacity component depends on the domain model and the narrow internal
|
||||||
model-client boundary, not on provider transport implementation. The model
|
model-client boundary, not on provider transport implementation. The model
|
||||||
client does not depend on registry or capacity configuration. The facade
|
client does not depend on registry or capacity configuration. The facade
|
||||||
@@ -71,7 +97,8 @@ on consumers or on Scriptorium.
|
|||||||
`internal/domain` owns source-neutral invariants for values shared across
|
`internal/domain` owns source-neutral invariants for values shared across
|
||||||
multiple input and execution boundaries, including execution-setting bounds,
|
multiple input and execution boundaries, including execution-setting bounds,
|
||||||
OpenAI-compatible base endpoints, session identifiers, and output-contract
|
OpenAI-compatible base endpoints, session identifiers, and output-contract
|
||||||
legality. Callers retain source parsing, required-field rules, other
|
legality, plus shared resource-identity syntax and qualification. Callers retain
|
||||||
|
source parsing, required-field rules, other
|
||||||
source-specific normalization, defaulting, error classification, and policy
|
source-specific normalization, defaulting, error classification, and policy
|
||||||
specific to their own boundary.
|
specific to their own boundary.
|
||||||
|
|
||||||
@@ -155,3 +182,21 @@ state.
|
|||||||
Do not list planned packages as implemented components. When implementation
|
Do not list planned packages as implemented components. When implementation
|
||||||
introduces a package, update the internal inventory and the owning contract or
|
introduces a package, update the internal inventory and the owning contract or
|
||||||
subsystem document in the same change.
|
subsystem document in the same change.
|
||||||
|
|
||||||
|
## Trust Boundaries And Threat Scope
|
||||||
|
|
||||||
|
Where useful, state the trust boundaries and threat scenarios relevant to the
|
||||||
|
application's adopted requirements, along with scenarios it explicitly excludes.
|
||||||
|
For example, distinguish handling untrusted application input from defending
|
||||||
|
against a privileged actor concurrently modifying the host filesystem.
|
||||||
|
|
||||||
|
Threat exclusions define the scope of the adopted requirements. Base defenses
|
||||||
|
and guarantees on those requirements and raise unsettled scope for a deliberate
|
||||||
|
decision under the [agent policy](agents.md).
|
||||||
|
|
||||||
|
Keep this discussion proportionate to the application. A separate threat
|
||||||
|
assessment or security document is optional when adopting this template.
|
||||||
|
For this library, the framework format and integration documents own credential
|
||||||
|
supply and external contracts; internal documents own implementation mechanisms.
|
||||||
|
Consumer-specific filesystem and security policy remains outside Promptkit's
|
||||||
|
boundary as described above.
|
||||||
|
|||||||
@@ -1,144 +1,139 @@
|
|||||||
# Documentation Policy
|
# Documentation Policy
|
||||||
|
|
||||||
**Base Policy Version:** 1.1
|
**Base Policy Version:** 1.11
|
||||||
|
|
||||||
**Last Updated:** 2026-09-23
|
**Last Updated**: 2026-09-27
|
||||||
|
|
||||||
|
**Upstream Source:** [documentation.md](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/documentation.md)
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
This policy assigns each Promptkit documentation topic to one canonical owner.
|
This policy assigns each documentation topic to one canonical owner. Its goal is
|
||||||
Its goal is to keep documentation for this reusable Go library accurate,
|
to keep this library's documentation accurate, concise, discoverable, and resistant
|
||||||
concise, discoverable, and resistant to drift for consumers, contributors,
|
to drift for users, operators, developers, integrators, and LLM coding agents.
|
||||||
maintainers, integrators, and coding agents.
|
|
||||||
|
|
||||||
## Core Rules
|
## Core Rules
|
||||||
|
|
||||||
### One Canonical Owner
|
### One Canonical Owner
|
||||||
|
|
||||||
Each authoritative fact belongs in one document or source form. A non-owning
|
Each authoritative fact belongs in one document. A non-owning document may give
|
||||||
document may give a short, stable summary for orientation, but it must link to
|
a short, stable summary for orientation, but it must link to the canonical owner
|
||||||
the canonical owner instead of repeating exact contracts.
|
for volatile details.
|
||||||
|
|
||||||
Volatile details include exported declarations, accepted inputs, defaults,
|
Volatile details include commands, flags, configuration fields and defaults,
|
||||||
schemas, file names, paths, error identities, retry behavior, and runtime
|
module keys, schemas, file names, paths, status codes, retry behavior, and
|
||||||
guarantees. If readers could reasonably treat a statement as a contract,
|
runtime guarantees. If readers could reasonably treat a statement as a
|
||||||
maintain its exact definition only in the owning source.
|
contract, maintain it only in the owning document.
|
||||||
|
|
||||||
### Current State, Decisions, And Future Work
|
### Current And Future Behavior
|
||||||
|
|
||||||
Outside `docs/roadmap/`, documentation describes implemented behavior only.
|
Outside `docs/roadmap/`, documentation describes implemented behavior only.
|
||||||
Partial features may be described only to their implemented boundary.
|
Partial features may be described only to their implemented boundary.
|
||||||
|
|
||||||
An accepted architecture decision may describe an approved direction before it
|
Architectural constraints may govern future changes. Describe current structure
|
||||||
is implemented, but acceptance is not evidence that the behavior exists.
|
from implemented behavior.
|
||||||
Current-state documents change when the implementation lands. Temporary
|
|
||||||
roadmaps own future work, sequencing, and implementation status; they do not
|
ADRs may record an accepted architectural decision before implementation. The
|
||||||
replace durable policies or current contracts.
|
roadmap owns implementation status and sequencing until the decision is
|
||||||
|
implemented. Current architecture, user, operator, integration, and internal
|
||||||
|
documentation are updated when the behavior lands.
|
||||||
|
|
||||||
### Audience And Detail
|
### Audience And Detail
|
||||||
|
|
||||||
Write for the document's stated audience and include only the detail needed for
|
Write for the document's stated audience and include only the detail needed for
|
||||||
its owned topic. Consumer guidance should not expose incidental implementation
|
its owned topic. Keep user and operator docs focused on using and operating the
|
||||||
detail. Contributor documentation should link to public contracts and durable
|
application. Developer docs should link to user-facing and external contracts
|
||||||
policies instead of restating them.
|
for their definitions.
|
||||||
|
|
||||||
### Links
|
### Examples
|
||||||
|
|
||||||
Use descriptive link text and repository-relative links for repository
|
Complete copyable files belong in `examples/`. Documentation may use the
|
||||||
documents. Link to the canonical owner rather than to a duplicate summary.
|
smallest illustrative snippet needed to explain its owned topic, but should link
|
||||||
Check every added or changed link and repair or remove links when their target
|
to maintained examples for complete copyable files.
|
||||||
moves or is retired.
|
|
||||||
|
|
||||||
### Examples And Code Fences
|
Examples must be valid, secret-free, and tested where practical. Commands and
|
||||||
|
configuration used in documentation should match the application.
|
||||||
Complete copyable files belong in `examples/` when maintained examples exist.
|
|
||||||
Documentation may use the smallest illustrative snippet needed for its owned
|
|
||||||
topic, but should link to a maintained example instead of embedding a second
|
|
||||||
complete copy.
|
|
||||||
|
|
||||||
Examples must be valid, secret-free, and tested where practical. Commands,
|
|
||||||
imports, and Go snippets must match the implemented library. Use a language tag
|
|
||||||
on fenced code blocks and make clear when a fragment is illustrative rather
|
|
||||||
than directly runnable.
|
|
||||||
|
|
||||||
### Security And Privacy
|
### Security And Privacy
|
||||||
|
|
||||||
Documentation and examples must not contain real credentials, private keys,
|
Documentation and examples must not contain real credentials, private keys,
|
||||||
private environment dumps, sensitive source material, or private
|
private environment dumps, sensitive source material, or private infrastructure
|
||||||
infrastructure details unless intentionally public. Document secret-handling
|
details unless intentionally public. Document secret-handling mechanisms, not
|
||||||
mechanisms, not secret values.
|
secret values.
|
||||||
|
|
||||||
## Canonical Ownership
|
## Canonical Ownership
|
||||||
|
|
||||||
| Topic | Canonical owner | Owned content | Content owned elsewhere |
|
| Topic | Canonical owner | Owned content | Content owned elsewhere |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Project orientation | `README.md` | What Promptkit is, its current usability, module identity, license summary, a minimal successful quickstart, and links onward. | Exact API contracts, contributor procedures, architecture detail, and release steps. |
|
| Product orientation and minimal end-to-end quickstart | `README.md` | Purpose, value, shortest successful invocation, and navigation, including licensing and attribution. | Complete command reference, configuration reference, operational procedures, implementation detail. |
|
||||||
| Contributor workflow | `docs/development.md` | Task-oriented reading guide, local workflow, validation commands, and repository hygiene. | Architecture rules, API semantics, subsystem behavior, and release procedure. |
|
| Contributor entry point | `docs/development.md` | Task-oriented reading guide, contributor orientation, baseline validation commands, and canonical links. | Package inventory, architecture rules, subsystem behavior, detailed change recipes. |
|
||||||
| Current architecture | `docs/policy/architecture.md` | System shape, normative ownership, dependency direction, package boundaries, invariants, and non-goals. | Concrete component inventory, implementation mechanics, contributor procedures, decision history, and future work. |
|
| Application architecture policy | `docs/policy/architecture.md` | System shape, normative ownership, dependency direction, boundaries, invariants, safety properties, and non-goals. | Concrete package inventory, implementation mechanics, contributor procedures, decision history, future work. |
|
||||||
| Agent policy | `docs/policy/agents.md` | Agent implementation discipline, proportionality, idiomatic simplicity, local commit authorization and discipline, and rules against inventing restrictions, guarantees, or repository policy. | Public contracts, architecture, testing, and contributor procedures. |
|
| Policy-set provenance and upstream navigation | `docs/policy/meta.md` | Policy-set origin, local authority, adoption baselines, and upstream adoption and update links. | Task-specific reading triggers, detailed adoption procedures, and formal attribution notices. |
|
||||||
| Documentation organization | `docs/policy/documentation.md` | Documentation ownership, audience boundaries, maintenance rules, and document lifecycle. | Library architecture or runtime behavior. |
|
| Agent policy | `docs/policy/agents.md` | Proportionate, idiomatic implementation; rules against inventing requirements, restrictions, guarantees, or policy. | Application contracts, architecture, testing policy, and contributor procedures. |
|
||||||
| Testing policy | `docs/policy/testing.md` | Test philosophy, risk-based sufficiency, test boundaries, doubles, coverage guidance, regression policy, and test maintenance. | Subsystem behavior, exact public contracts, subsystem-specific test inventories, and implementation plans. |
|
| Documentation policy | `docs/policy/documentation.md` | Documentation ownership, audience boundaries, maintenance rules, and ADR/document lifecycle. | Application architecture or product behavior. |
|
||||||
| Release policy | `docs/policy/release.md` | Versioning, compatibility, release-note requirements, validation obligations, immutability, recovery, and adoption scope. | Executable commands, publication mechanics, and historical release summaries. |
|
| Testing policy | `docs/policy/testing.md` | Test philosophy, risk sufficiency, boundaries, doubles, coverage, regressions, and test maintenance. | Subsystem behavior, application contracts, subsystem-specific test inventories, and implementation plans. |
|
||||||
| Release procedure | `docs/release.md` | Maintainer prerequisites, candidate preparation, concrete publication guards and commands, verification, and recovery steps. | Release-policy requirements, public API semantics, and historical summaries. |
|
| Release policy | `docs/policy/release.md` | Versioning, compatibility, release integrity and immutability, release documentation, proportionate gates, publication controls, completion, and required versus optional checks. | Repository-specific commands, product installation guidance, CLI version semantics, historical release summaries, and CI implementation detail. |
|
||||||
|
| Open source licensing and attribution policy | `docs/policy/oss.md` | License defaults and author choices, COPYRIGHT and attribution formats, license/notice links, discoverability, maintenance, and concrete exceptions. | Component entries, license texts, release commands, and dependency security review. |
|
||||||
|
| Project copyright and license grant | `COPYRIGHT` | Project identity, copyright holders and years, SPDX expression, grant, warranty disclaimer, and third-party statement. | Complete license texts and upstream component attribution. |
|
||||||
|
| Project license texts | `LICENSE` | Complete project and incorporated companion license texts, with required project-specific fields. | General licensing policy and upstream component attribution. |
|
||||||
|
| Upstream component attribution | `docs/oss/attribution.md` | House Format component entries: identity, material scope and modifications where applicable, copyright, license, required notices, and license, notice, and source links. | General attribution policy, exception reasoning, research narratives, compliance commentary, and complete standard license texts. |
|
||||||
|
| Standard dependency license texts | `docs/oss/licenses/` | Complete, verbatim standard license texts shared by applicable entries. | Project-specific upstream notices, attribution prose, and policy interpretation. |
|
||||||
|
| Repository-specific release procedure | `docs/release.md` | Maintainer prerequisites, candidate preparation, validation and publication commands and automated guards, asynchronous deliverables, delivery status, optional consumer verification, and recovery. | General release requirements, product installation guidance, historical release summaries, and current compatibility contracts. |
|
||||||
|
| Consumer guidance for the public Go API (if applicable) | `docs/consumers/` | Task-oriented public Go API usage, minimal examples, and consumer responsibilities. | HTTP wire semantics, external protocol contracts, internal implementation detail. |
|
||||||
|
| Integration contracts for external components (if applicable) | `docs/integrations/` | External formats and protocols, upstream and downstream contracts, logical output paths and schemas, media types, and compatibility. | Physical runtime placement and lifecycle, internal transformations, CLI syntax, configuration defaults. |
|
||||||
|
| Inventory of implemented internal packages | `docs/internal/overview.md` | Implemented packages and components, responsibilities, and links to focused internal docs. | Normative architecture, contributor reading policy, external contracts. |
|
||||||
|
| Behavior and architecture of internal packages | Other files under `docs/internal/` | Implementation flow, collaborators, state transitions, package-local guarantees and failures, and relevant tests. | Global architecture invariants, configuration definitions and defaults, external schemas, operator procedures. |
|
||||||
|
| Architectural decision records | `docs/adr/` | Significant decisions, context, alternatives, rationale, consequences, and supersession history. | Current behavior reference, implementation status, task sequencing. |
|
||||||
|
| Future work and implementation status | `docs/roadmap/` | Proposed, accepted, deferred, or rejected work; implementation status, sequencing, and task breakdowns. | Implemented behavior reference and architectural decision rationale. |
|
||||||
|
| Release-note history | `docs/releases/` | One checked-in summary per source release under the procedure; the note at its immutable tag is the release record. | Current commands, behavior, contracts, and compatibility definitions. |
|
||||||
|
| Complete copyable artifacts | `examples/` | Maintained configuration, inputs, and other copyable or runnable files. | Field-by-field reference, command reference, prose explanation. |
|
||||||
| Exact exported Go API | Go declarations and GoDoc, as APIs are implemented | Exported names, signatures, types, values, errors, and exact behavioral contracts. | Task-oriented consumer walkthroughs, implementation details, and future API proposals. |
|
| Exact exported Go API | Go declarations and GoDoc, as APIs are implemented | Exported names, signatures, types, values, errors, and exact behavioral contracts. | Task-oriented consumer walkthroughs, implementation details, and future API proposals. |
|
||||||
| Framework file formats | `docs/formats.md` | Prompt-definition and profile YAML fields, schema references, defaults, validation modes, built-in profiles, credentials, and file-to-request precedence. | Exported Go declarations, outbound wire behavior, internal parsing mechanics, and application configuration. |
|
| Framework file formats | `docs/formats.md` | Prompt-definition and profile YAML fields, schema references, defaults, validation modes, credentials, and file-to-request precedence. | Exported Go declarations, outbound wire behavior, internal parsing mechanics, and application configuration. |
|
||||||
| Consumer guidance | `docs/consumers/`, when consumer workflows require dedicated guidance | Task-oriented use of implemented public APIs, minimal examples, and consumer responsibilities. | Exact exported declarations and internal mechanics. |
|
|
||||||
| Durable integration contracts | `docs/integrations/`, when integrations exist | External formats and protocols, compatibility behavior, and upstream or downstream responsibilities. | Internal transformations and public Go declarations. |
|
|
||||||
| Release-note history | `docs/releases/` | Mandatory version-matched historical summaries for releases made under the adopted release policy; the note at the immutable tag records that release. | Current APIs, behavior, formats, compatibility contracts, and release procedures. |
|
|
||||||
| Implemented component inventory | `docs/internal/overview.md` | Current packages and components, their implemented responsibilities, and links to focused internal documents. | Normative architecture, contributor workflow, external contracts, and proposed components. |
|
|
||||||
| Internal subsystem behavior | Other files under `docs/internal/`, when a subsystem needs durable detail | Implementation flow, internal collaborators and state transitions, package-local guarantees and failures, and relevant tests. | Global architecture invariants, public API definitions, and future package plans. |
|
|
||||||
| Architectural decision history | `docs/adr/`, when repository-local decisions require records | Significant decisions, context, alternatives, rationale, consequences, and supersession history. | Current behavior reference, implementation status, and task sequencing. |
|
|
||||||
| Temporary feature roadmaps | `docs/roadmap/`, while planned work needs coordination | Proposed or accepted scope, sequencing, gates, and implementation status. | Implemented behavior reference and durable decision rationale. |
|
|
||||||
| Complete copyable artifacts | `examples/` | Valid inputs, Go programs, and other files intended to be copied or run. | Field-by-field reference, exact API declarations, and prose explanation. |
|
|
||||||
|
|
||||||
Conditional owners do not require placeholder files or directories. Create a
|
Create supporting documents for existing interfaces and responsibilities when
|
||||||
consumer, integration, release, subsystem, ADR, roadmap, or example document
|
substantive content is available.
|
||||||
only when the corresponding implemented interface, release, decision, planned
|
|
||||||
effort, or maintained artifact exists.
|
|
||||||
|
|
||||||
## Boundary Rules
|
## Boundary Rules
|
||||||
|
|
||||||
### Orientation
|
### Orientation
|
||||||
|
|
||||||
The README owns project orientation. The development guide routes
|
Link among orientation, architecture, and internal documents; maintain each
|
||||||
contributors. Architecture owns normative structure. The internal overview
|
package or behavior description in its canonical owner.
|
||||||
owns the current concrete component map. These documents may link to one
|
|
||||||
another but must not maintain parallel package or behavior descriptions.
|
|
||||||
|
|
||||||
### Public Contracts And Implementation
|
### Public Contracts And Implementation
|
||||||
|
|
||||||
Go declarations and GoDoc own exact exported API contracts once those APIs
|
Go declarations and GoDoc own exact exported Go API contracts. The
|
||||||
exist. Consumer and integration documents explain how to use those contracts
|
[framework format reference](../formats.md) owns prompt, profile, and schema
|
||||||
for a task. Internal documents explain how Promptkit implements them. Internal
|
formats; integration documents own external wire behavior. Consumer and
|
||||||
documentation may identify a public type or external format as a dependency,
|
internal documents link to those owners for exact definitions.
|
||||||
but must link to its canonical definition rather than restate it.
|
|
||||||
|
|
||||||
The [framework format reference](../formats.md) owns exact prompt, profile, and
|
### Releases
|
||||||
schema-file contracts. Integration documents own external wire formats.
|
|
||||||
|
|
||||||
### Release Documentation
|
The release policy defines requirements and invariants that every release must
|
||||||
|
satisfy. The repository-specific release procedure defines the commands and
|
||||||
|
workflow maintainers use to satisfy that policy. Release notes record history
|
||||||
|
at immutable tags. Maintain current behavior in its canonical documentation,
|
||||||
|
update it with the corresponding behavior changes, and keep release procedures
|
||||||
|
consistent with adopted policy.
|
||||||
|
|
||||||
The [release policy](release.md) owns release requirements; the
|
### Project Licensing And Upstream Attribution
|
||||||
[repository release procedure](../release.md) owns their concrete execution.
|
|
||||||
Version-matched checked-in notes are required for future releases under that
|
|
||||||
policy. The source tag is the release identity, and the note in its immutable
|
|
||||||
tree is the historical summary; annotated tag messages complement that record.
|
|
||||||
|
|
||||||
Release notes do not own current API or behavior contracts. Link to the
|
README and release documents link to the canonical licensing and attribution
|
||||||
canonical owners for details instead of reproducing those contracts. Published
|
materials under the [OSS policy](oss.md). The release procedure owns URL
|
||||||
notes are not disposable guidance and must not be replaced under the same
|
construction and publication mechanics.
|
||||||
version. Corrections belong in a subsequent release.
|
|
||||||
|
|
||||||
Adoption is prospective. Previously published tags and historical notes retain
|
### Contracts And Implementation
|
||||||
their original records and are not reformatted or rewritten to satisfy the new
|
|
||||||
requirements.
|
Integration and API documents define externally observable shapes and
|
||||||
|
semantics. Internal documents explain how this application implements or consumes those
|
||||||
|
contracts. Internal docs may name a field, file, or protocol to identify a
|
||||||
|
dependency, but must link to its canonical contract for the definition.
|
||||||
|
|
||||||
### Security Topics
|
### Security Topics
|
||||||
|
|
||||||
This policy owns what documentation and examples may contain. Architecture owns
|
This policy owns what documentation and examples may contain. Architecture owns
|
||||||
library security boundaries and invariants. Public declarations and integration
|
library security boundaries. Public declarations and integration documents own
|
||||||
documents own consumer-visible security contracts. Internal documents own
|
consumer-visible contracts; internal documents own implementation mechanisms.
|
||||||
implementation mechanisms only.
|
|
||||||
|
|
||||||
## Architecture Decision Records
|
## Architecture Decision Records
|
||||||
|
|
||||||
@@ -153,31 +148,35 @@ Use sequentially numbered ADR filenames such as
|
|||||||
6. alternatives considered;
|
6. alternatives considered;
|
||||||
7. consequences.
|
7. consequences.
|
||||||
|
|
||||||
Use one of these statuses:
|
Treat the decision content of an accepted ADR as immutable. When a decision
|
||||||
|
changes, create a new ADR and update the earlier ADR's status to superseded.
|
||||||
|
Rejected architectural alternatives belong in the ADR; rejected product ideas
|
||||||
|
belong in the roadmap.
|
||||||
|
|
||||||
- **Proposed:** the decision is under consideration and may change;
|
## Maintenance
|
||||||
- **Accepted:** the decision is approved, whether or not implementation is
|
|
||||||
complete;
|
|
||||||
- **Rejected:** the proposed decision was considered and not adopted;
|
|
||||||
- **Superseded:** a later accepted ADR replaces the accepted decision.
|
|
||||||
|
|
||||||
A proposed ADR transitions to Accepted or Rejected. An Accepted ADR transitions
|
When behavior changes, update its canonical owner in the same change. If
|
||||||
to Superseded only when a later Accepted ADR replaces it. An ADR may be created
|
ownership moves, remove the old definition and replace it with a link where
|
||||||
as Accepted when the decision has already been made.
|
navigation remains useful.
|
||||||
|
|
||||||
Treat the decision content of an Accepted ADR as immutable. A changed decision
|
Before completing documentation work:
|
||||||
requires a later ADR rather than a rewrite of the accepted record. A Superseded
|
|
||||||
ADR must link to its replacement, and the replacement must link back. Rejected
|
- verify affected behavior and examples;
|
||||||
architectural alternatives belong in the ADR; rejected feature ideas belong in
|
- check commands, flags, fields, defaults, schemas, and paths against their
|
||||||
a roadmap when they need to be retained.
|
implementation;
|
||||||
|
- keep unimplemented behavior in the roadmap, subject to the ADR exception;
|
||||||
|
- remove stale references and validate links;
|
||||||
|
- confirm that non-owning documents summarize and link rather than redefine;
|
||||||
|
- confirm that no secrets or sensitive private data were added.
|
||||||
|
|
||||||
|
## Promptkit Documentation Scope
|
||||||
|
|
||||||
|
Promptkit is a Go library, so exported GoDoc and framework formats replace
|
||||||
|
application CLI, configuration, operations, and HTTP-service owners. Reading
|
||||||
|
triggers belong in the development guide, not this topic map.
|
||||||
|
|
||||||
## Document Lifecycle
|
## Document Lifecycle
|
||||||
|
|
||||||
Create durable current-state documentation with the implementation it
|
|
||||||
describes. Update its canonical owner in the same change when behavior changes.
|
|
||||||
If ownership moves, remove the old definition and leave a link where navigation
|
|
||||||
remains useful.
|
|
||||||
|
|
||||||
Roadmaps are temporary coordination documents. When their work is complete,
|
Roadmaps are temporary coordination documents. When their work is complete,
|
||||||
record completion, move any still-useful decisions or contracts to their
|
record completion, move any still-useful decisions or contracts to their
|
||||||
durable owners, update incoming links, and archive or remove the roadmap
|
durable owners, update incoming links, and archive or remove the roadmap
|
||||||
@@ -187,13 +186,9 @@ second current-state reference.
|
|||||||
Retain release notes as historical records under the release policy. Do not
|
Retain release notes as historical records under the release policy. Do not
|
||||||
apply temporary-roadmap retirement rules to published release notes.
|
apply temporary-roadmap retirement rules to published release notes.
|
||||||
|
|
||||||
Before completing documentation work:
|
For Promptkit ADRs, retain the existing Proposed, Accepted, Rejected, and
|
||||||
|
Superseded statuses. Proposed records may become Accepted or Rejected;
|
||||||
- verify affected behavior and examples;
|
Accepted records may become Superseded, with links between the earlier record
|
||||||
- check commands, imports, declarations, defaults, schemas, and paths against
|
and its replacement. Records may be created as Accepted when the decision has
|
||||||
their implementation;
|
already been made. These lifecycle rules preserve existing local document
|
||||||
- keep unimplemented behavior in a roadmap, subject to the ADR exception;
|
semantics.
|
||||||
- validate links and fenced examples;
|
|
||||||
- confirm non-owning documents summarize and link rather than redefine;
|
|
||||||
- remove stale or unsupported claims; and
|
|
||||||
- confirm that no secrets or sensitive private data were added.
|
|
||||||
|
|||||||
27
docs/policy/meta.md
Normal file
27
docs/policy/meta.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Policy Set Provenance
|
||||||
|
|
||||||
|
**Base Policy Version:** 1.0
|
||||||
|
|
||||||
|
**Last Updated**: 2026-09-27
|
||||||
|
|
||||||
|
**Upstream Source:** [meta.md](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/meta.md)
|
||||||
|
|
||||||
|
## Origin And Local Authority
|
||||||
|
|
||||||
|
This repository's policy set is adapted from
|
||||||
|
[Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy).
|
||||||
|
The adopted local policies govern work in this repository. Upstream revisions
|
||||||
|
become local policy through deliberate adoption, preserving local decisions.
|
||||||
|
Each policy's `Upstream Source` identifies its adopted baseline.
|
||||||
|
|
||||||
|
## Adoption And Updates
|
||||||
|
|
||||||
|
For policy adoption, upstream revision reviews, and updates, consult the
|
||||||
|
[upstream adoption and update guide](https://gitea.maximumdirect.net/eric/policy/src/branch/main/docs/adoption.md).
|
||||||
|
That guide and the reference example collection are maintained upstream and
|
||||||
|
accessed through links. Keep discovery links pointed at the current upstream
|
||||||
|
project and guide; keep adopted-source and example links pinned to commits.
|
||||||
|
|
||||||
|
The local [developer guide](../development.md) routes tasks to applicable
|
||||||
|
policies. Formal credit and licensing notices for the copied policy material
|
||||||
|
belong in the local [attribution document](../oss/attribution.md).
|
||||||
228
docs/policy/oss.md
Normal file
228
docs/policy/oss.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# Open Source Licensing And Attribution Policy
|
||||||
|
|
||||||
|
**Base Policy Version:** 1.8
|
||||||
|
|
||||||
|
**Last Updated**: 2026-11-02
|
||||||
|
|
||||||
|
**Upstream Source:** [oss.md](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/oss.md)
|
||||||
|
|
||||||
|
## Purpose And Scope
|
||||||
|
|
||||||
|
This policy defines project licensing conventions and a repeatable House Format
|
||||||
|
for upstream attribution and license notices. It covers the project's own code
|
||||||
|
and upstream components incorporated into or distributed with the application
|
||||||
|
or library, including relevant transitive dependencies and copied code,
|
||||||
|
documentation, or assets.
|
||||||
|
Determine treatment from the component's terms and actual use.
|
||||||
|
|
||||||
|
Consulting development guidance or using tooling alone does not require an
|
||||||
|
attribution entry. Copying or adapting documentation for inclusion in the
|
||||||
|
repository is different: preserve its applicable attribution and license terms.
|
||||||
|
A material's role as development guidance does not exempt copied documents.
|
||||||
|
Apply any express permissions for attribution-free reuse of example formatting.
|
||||||
|
|
||||||
|
The baseline is a publicly available source repository with releases published
|
||||||
|
alongside it. The format is independent of language, forge, and build system.
|
||||||
|
It does not require attribution for every development tool, establish license
|
||||||
|
compatibility, or resolve every obligation arising from use or distribution.
|
||||||
|
|
||||||
|
## Canonical Locations
|
||||||
|
|
||||||
|
The [documentation policy](documentation.md) assigns ownership:
|
||||||
|
|
||||||
|
- `COPYRIGHT`: project name, copyright statements, SPDX expression, grant,
|
||||||
|
warranty disclaimer, and third-party statement.
|
||||||
|
- `LICENSE` contains the complete, unmodified project license text, including
|
||||||
|
incorporated companion text. Dependency attribution does not relicense the project.
|
||||||
|
- `docs/oss/attribution.md`: canonical component entries.
|
||||||
|
- `docs/oss/licenses/`: complete, verbatim standard and applicable companion texts.
|
||||||
|
|
||||||
|
Use recognizable license filenames, such as `Apache-2.0.txt` or `LGPL-3.0.txt`.
|
||||||
|
A shared standard text may serve multiple entries. Keep explanations outside
|
||||||
|
license texts and preserve distinct versions and exceptions.
|
||||||
|
|
||||||
|
## Upstream Reference Examples
|
||||||
|
|
||||||
|
Use the commit-pinned upstream examples to create actual project notices.
|
||||||
|
Retain their links and keep the example collection upstream; keep applicable
|
||||||
|
license texts locally as specified above.
|
||||||
|
|
||||||
|
## Project Licensing
|
||||||
|
|
||||||
|
Unless the author specifies otherwise, use **GPL version 3 or later**
|
||||||
|
(`GPL-3.0-or-later`) for applications and **LGPL version 3 or later**
|
||||||
|
(`LGPL-3.0-or-later`) for libraries. An author's explicit choice takes precedence:
|
||||||
|
for example, a one-off script may use MIT. Such a choice requires no exception
|
||||||
|
rationale, additional approval, or compensating controls under this policy.
|
||||||
|
|
||||||
|
These defaults guide new license selection. Adopting this policy does not
|
||||||
|
relicense existing code or override third-party terms. Preserve an existing
|
||||||
|
license unless the author deliberately changes it. For mixed-purpose projects,
|
||||||
|
follow the author's selected scope rather than inferring different licenses for
|
||||||
|
individual packages.
|
||||||
|
|
||||||
|
Use the upstream [application COPYRIGHT example](https://gitea.maximumdirect.net/eric/policy/src/commit/a469273adda336625f96b9d769ea27b33999f146/examples/licensing/application/COPYRIGHT)
|
||||||
|
or [library COPYRIGHT example](https://gitea.maximumdirect.net/eric/policy/src/commit/a469273adda336625f96b9d769ea27b33999f146/examples/licensing/library/COPYRIGHT)
|
||||||
|
to create the project’s root `COPYRIGHT` file.
|
||||||
|
Use verified project names, contribution years, and copyright holders.
|
||||||
|
For another selected license, adapt the grant, SPDX expression, disclaimer, and
|
||||||
|
text references together to reflect the author's selected grant.
|
||||||
|
|
||||||
|
For GPLv3 projects, copy the complete GPLv3 text into `LICENSE`. For LGPLv3
|
||||||
|
projects, place the complete LGPLv3 text followed by the complete GPLv3 text in
|
||||||
|
`LICENSE`; the LGPL incorporates the GPL terms. Keep the texts unmodified and
|
||||||
|
place project-specific statements in `COPYRIGHT`. For the default grants, the
|
||||||
|
grant and SPDX expression in `COPYRIGHT` must expressly identify `or-later`;
|
||||||
|
follow the author’s choice if a different grant is selected.
|
||||||
|
|
||||||
|
For other licenses, provide their complete text and any required project-specific
|
||||||
|
fields; the standard reference collection remains unchanged. Keep `COPYRIGHT`,
|
||||||
|
`LICENSE`, SPDX expressions, and README descriptions consistent with the actual
|
||||||
|
grant. This convention introduces no blanket requirement for per-file headers;
|
||||||
|
preserve notices or add them where the applicable terms require them.
|
||||||
|
|
||||||
|
Identify separately licensed documentation or assets in the project grant, with
|
||||||
|
links to their attribution and license information. An application's default
|
||||||
|
code license does not override the terms of incorporated documentation.
|
||||||
|
|
||||||
|
## Upstream Attribution House Format
|
||||||
|
|
||||||
|
Begin `docs/oss/attribution.md` with `# Open Source Attribution Notices`,
|
||||||
|
followed directly by component entries. These comprise the complete document.
|
||||||
|
|
||||||
|
Each entry presents these five fields in order, with blank lines between them:
|
||||||
|
|
||||||
|
1. **Project name** as a level-two heading. An optional identification paragraph
|
||||||
|
may follow, such as `Selected module: MODULE at VERSION.` with the module and
|
||||||
|
version in backticks. For copied documentation, a concise `Material:` sentence
|
||||||
|
may identify the affected files and whether they were adapted or copied
|
||||||
|
without modification. A factual qualifier such as `(transitive dependency)`
|
||||||
|
is permitted. Limit optional identification prose to factual component and
|
||||||
|
material scope.
|
||||||
|
2. **Upstream copyright statement or statements**, preserving their wording.
|
||||||
|
Put multiple statements in separate paragraphs. Include this field when the
|
||||||
|
selected upstream source supplies an applicable statement; otherwise, omit it.
|
||||||
|
3. **SPDX license identifier or expression**, when applicable, using
|
||||||
|
`SPDX-License-Identifier: EXPRESSION`.
|
||||||
|
4. **License and notice links**, beginning `License and notices:`, separated
|
||||||
|
by ` | `, and ending with a period. Label local standard texts by license
|
||||||
|
name, such as `MIT` or `Apache 2.0`. Label upstream files by their actual
|
||||||
|
filenames, such as `COPYRIGHT`, `LICENSE`, `NOTICE`, or `PATENTS`.
|
||||||
|
5. **Source repository link**, rendered exactly as:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
[Source](COMMIT_URL).
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the upstream [attribution example](https://gitea.maximumdirect.net/eric/policy/src/commit/a469273adda336625f96b9d769ea27b33999f146/examples/licensing/attribution.md)
|
||||||
|
to create the project’s `docs/oss/attribution.md`.
|
||||||
|
The example illustrates omitted fields and multiple copyrights and licenses.
|
||||||
|
Replace the uppercase placeholders and illustrative URLs with actual component
|
||||||
|
information. Local license links are relative to the destination
|
||||||
|
`docs/oss/attribution.md`; copy the applicable texts into its `licenses/`
|
||||||
|
directory. Do not copy example entries for components the project does not use.
|
||||||
|
|
||||||
|
These are five fields, not a line limit. Omit the SPDX field when no applicable
|
||||||
|
identifier exists; identify nonstandard terms through the linked text. Preserve
|
||||||
|
licensing alternatives and exceptions, and record a selected option when
|
||||||
|
relevant through the factual license identification. Do not invent an identifier
|
||||||
|
or silently resolve an unsettled licensing choice.
|
||||||
|
|
||||||
|
Keep entries factual. Do not add agent-authored explanations of license
|
||||||
|
expressions, file-level license scope, research performed, retained source
|
||||||
|
notices, or compliance conclusions. Avoid filler such as "none is invented
|
||||||
|
here" and embellished link labels such as "upstream LICENSE", "additional
|
||||||
|
patent grant", or "at the imported commit". Preserve actual required upstream
|
||||||
|
content; this rule excludes editorial additions, not required notices. Policy
|
||||||
|
interpretation and exception reasoning belong in the adopted OSS policy or an
|
||||||
|
appropriate decision record, not the attribution document.
|
||||||
|
|
||||||
|
## License Texts And Upstream Links
|
||||||
|
|
||||||
|
Link standard license texts to their local copies. Link project-specific
|
||||||
|
notices to their upstream copies. Every upstream repository or notice link must
|
||||||
|
identify the exact commit corresponding to the
|
||||||
|
imported version, including the selected source revision for copied material.
|
||||||
|
|
||||||
|
Classify material according to its contents. An upstream `LICENSE`,
|
||||||
|
`COPYING`, or similar file may contain project-specific notices or exceptions
|
||||||
|
in addition to standard wording. Link both the local standard text and the
|
||||||
|
commit-specific upstream file when needed. Make companion license texts directly
|
||||||
|
reachable from the entry.
|
||||||
|
|
||||||
|
For project-specific license and attribution material, this policy accepts
|
||||||
|
direct links to the applicable upstream texts as satisfying reproduction,
|
||||||
|
inclusion, or copy requirements. This is the adopted house interpretation.
|
||||||
|
Agents must apply it without adding local-copy requirements or seeking repeated
|
||||||
|
approval solely because a license uses those terms.
|
||||||
|
|
||||||
|
This interpretation concerns notice reproduction. Concrete obligations such as
|
||||||
|
marking modified files or supplying corresponding source receive their own
|
||||||
|
applicable treatment. Preserve required notices already present in incorporated
|
||||||
|
source files; the central attribution entry does not authorize stripping them.
|
||||||
|
|
||||||
|
## Discoverability
|
||||||
|
|
||||||
|
End the project's README with two friendly statements linking to its actual
|
||||||
|
grant in `COPYRIGHT` and the attribution document. For example, for a project
|
||||||
|
with the stated license:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
PROJECT_NAME is licensed under the [LGPL v3.0 or later](COPYRIGHT).
|
||||||
|
|
||||||
|
[Open Source attribution notices](docs/oss/attribution.md) are available.
|
||||||
|
```
|
||||||
|
|
||||||
|
Each new checked-in release document and its hosted download page must end
|
||||||
|
with links whose visible text is `COPYRIGHT | LICENSE | ATTRIBUTION`. Use absolute
|
||||||
|
URLs to those files at the release's immutable tag or corresponding immutable source
|
||||||
|
revision so the links work in both locations. This release navigation is distinct
|
||||||
|
from upstream component links, which must use the imported commit.
|
||||||
|
|
||||||
|
Reuse the checked-in release document as the hosted release body where supported.
|
||||||
|
The release procedure owns concrete URLs and publication mechanics for delivering
|
||||||
|
the links through the selected channel. Do not maintain duplicate notice lists.
|
||||||
|
These conventions do not by themselves require separate downloadable license
|
||||||
|
assets, source bundles, or a different binary package format.
|
||||||
|
|
||||||
|
## Maintenance And Exceptions
|
||||||
|
|
||||||
|
Update project licensing statements together when the author changes the grant
|
||||||
|
or its scope. Update upstream notices in the change that adds, removes, upgrades,
|
||||||
|
copies, or changes the distribution of a relevant component. Check the selected source's license
|
||||||
|
and notices rather than assuming they are unchanged. Update affected entries,
|
||||||
|
commit links, and texts; retain historical versions through Git.
|
||||||
|
|
||||||
|
Check affected statements, faithful standard-text copies, and link destinations
|
||||||
|
as part of ordinary change review. For a future release tag, check intended file
|
||||||
|
paths against the candidate source; the tag need not resolve remotely before
|
||||||
|
publication. Do not add publication-time network checks, CI polling, recurring
|
||||||
|
legal sign-offs, mandatory scanners, or a full software bill of materials merely
|
||||||
|
to implement this policy.
|
||||||
|
|
||||||
|
Use the House Format routinely. Raise an exception only for a concrete obligation
|
||||||
|
or unresolved rights question it does not address, including any arising from a
|
||||||
|
particular distribution channel. Record the issue and selected treatment in the
|
||||||
|
adopted OSS policy or an appropriate decision record. Attribution entries
|
||||||
|
contain the resulting factual identification and required material, without
|
||||||
|
repeating that reasoning. Resolve the issue before the affected use or
|
||||||
|
distribution without reopening settled choices.
|
||||||
|
Common ownership of projects is neither required nor a blanket exception.
|
||||||
|
|
||||||
|
Apply the formatting convention prospectively. Do not rewrite published tags,
|
||||||
|
notes, binaries, or checksums to retrofit it. Address actual omissions deliberately
|
||||||
|
under the applicable terms and the [release correction policy](release.md#immutability-and-failure-recovery).
|
||||||
|
Ordinary attribution maintenance and release links are preparation work, not new
|
||||||
|
release approval gates.
|
||||||
|
|
||||||
|
## Promptkit Adoption
|
||||||
|
|
||||||
|
Promptkit retains its LGPL-3.0-or-later library grant in
|
||||||
|
[COPYRIGHT](../../COPYRIGHT). Adapted policy documentation has the separate
|
||||||
|
scope identified there and in [attribution](../oss/attribution.md).
|
||||||
|
|
||||||
|
Promptkit publishes source-only Go module tags and Gitea release pages, with no
|
||||||
|
binaries. Each new hosted page reuses the complete checked-in release note,
|
||||||
|
including its licensing footer. The [release procedure](../release.md) owns
|
||||||
|
source publication, hosted delivery, and concrete footer URLs. Previously
|
||||||
|
published notes and tags retain their original records.
|
||||||
@@ -1,146 +1,292 @@
|
|||||||
# Release Policy
|
# Release Policy
|
||||||
|
|
||||||
**Base Policy Version:** 1.0
|
**Base Policy Version:** 1.6
|
||||||
|
|
||||||
**Last Updated:** 2026-09-23
|
**Last Updated**: 2026-11-02
|
||||||
|
|
||||||
## Purpose And Adoption Scope
|
**Upstream Source:** [release.md](https://gitea.maximumdirect.net/eric/policy/src/commit/e833faddf120c789a9b45348edaf75720de16348/docs/policy/release.md)
|
||||||
|
|
||||||
This policy owns Promptkit's release requirements and compatibility rules.
|
## Purpose
|
||||||
The [release procedure](../release.md) owns the commands used to satisfy them.
|
|
||||||
This scoped adoption applies to releases prepared after its adoption on
|
|
||||||
2026-09-23. Published tags and historical notes are not rewritten to retrofit
|
|
||||||
the new note format.
|
|
||||||
|
|
||||||
## Release Model And Compatibility
|
This policy defines a proportionate workflow for validating and publishing an
|
||||||
|
intended source revision under a fresh version. Releases should require little
|
||||||
|
routine intervention, avoid repeated work, and preserve published contents.
|
||||||
|
|
||||||
Promptkit publishes source-only Go module releases from committed, pushed
|
Adopt stronger verification or reproducibility requirements through an explicit
|
||||||
`main` candidates through annotated `vMAJOR.MINOR.PATCH` tags. It publishes
|
project-specific decision justified by the intended audience and support needs.
|
||||||
no binaries, hosted releases, or other artifacts and uses maintainer-run
|
|
||||||
validation rather than hosted CI. Versions must be unused locally and on
|
|
||||||
`origin`; the existing procedure accepts stable version tags only.
|
|
||||||
|
|
||||||
Before v1, minor releases may change the public API and patches preserve
|
The repository-specific commands belong in `docs/release.md`. Historical release
|
||||||
compatibility within their minor line, subject to the catalog-data provision
|
summaries belong in `docs/releases/`. The procedure must agree with the adopted
|
||||||
below. Release notes must identify compatibility impact, public API changes,
|
policy and link to it rather than introduce additional release requirements.
|
||||||
and required consumer action. The source tag identifies the exact release;
|
|
||||||
the checked-in note at that tag records its historical summary.
|
|
||||||
|
|
||||||
## Catalog Profile Compatibility
|
## Release Model And Completion
|
||||||
|
|
||||||
External catalog profile data may be added, removed, renamed, or modified
|
A source release identifies one exact committed source revision through an
|
||||||
without deprecation or advance notice. This exception is limited to profile
|
immutable version tag and a version-matched, checked-in release note. The
|
||||||
data imported from the maintained catalog modules. It does not weaken ordinary
|
primary release branch is `main` unless the adopted policy specifies otherwise.
|
||||||
compatibility for Promptkit's public Go API, configuration and format semantics,
|
The procedure must name the authoritative publication remote.
|
||||||
runtime behavior, catalog-module APIs, manifest schema, backend definitions, or
|
|
||||||
consumer-owned definitions.
|
|
||||||
|
|
||||||
Within the catalog versions selected and tested by a Promptkit release, and
|
A successful push of the intended tag, with its source commit available on the
|
||||||
unless a consumer overrides the name or its base, Promptkit keeps the literal
|
authoritative primary branch, completes the release. The source commit may be
|
||||||
profile IDs `promptkit-openrouter-standard` and
|
pushed before the tag or published together with it through an atomic push.
|
||||||
`promptkit-openrouter-auto` available. The names do not promise a permanent
|
The procedure must choose and document the supported approach.
|
||||||
model, backend, routing behavior, settings, cost, output, or upstream
|
|
||||||
availability. The [framework format reference](../formats.md#built-in-profile-catalog)
|
|
||||||
owns their current definitions.
|
|
||||||
|
|
||||||
`v0.10.0` is the initial minor release for this compatibility transition.
|
Release commands must finish at successful publication. They must not wait for
|
||||||
Afterward, catalog-only updates and adjustments to the current selections may
|
or poll CI, require successful CI status, inspect downloadable artifacts, or
|
||||||
ship in a patch release when all other compatibility obligations remain intact.
|
check consumer installation or Go module availability as completion gates.
|
||||||
External profile removals can require consumer action; they are not automatic
|
A failed or ambiguous push requires resolving the authoritative remote state
|
||||||
migrations.
|
before declaring success or retrying; successful pushes do not require a
|
||||||
|
separate remote read-back gate.
|
||||||
|
|
||||||
Catalog adoption and release validation remain local and offline. They do not
|
Repositories may publish source only. Additional binaries, images, packages,
|
||||||
probe provider endpoints, request model lists, or make inference calls.
|
checksums, or other declared deliverables may be published asynchronously from
|
||||||
|
the tagged source. The procedure must identify those deliverables and the
|
||||||
|
responsibility of their automation. Published deliverables must be traceable
|
||||||
|
to that source and identify the same version.
|
||||||
|
|
||||||
## Release Documentation
|
Track source-release completion and artifact delivery separately. Report delivery
|
||||||
|
failures and optional investigations under artifact-delivery status.
|
||||||
|
|
||||||
|
## Versioning And Compatibility
|
||||||
|
|
||||||
|
Stable releases use Semantic Versioning in the form `vMAJOR.MINOR.PATCH`, with
|
||||||
|
no leading zeroes. Prereleases require an explicit adopted policy defining
|
||||||
|
their format and use.
|
||||||
|
|
||||||
|
The selected version must be unused locally and on the authoritative remote.
|
||||||
|
Do not require queries to every configured remote or publication service.
|
||||||
|
A version known to have already been published must not be reused, even if the
|
||||||
|
normal guards do not inspect that channel. Adding a distribution channel
|
||||||
|
requires a deliberate update to the policy and procedure where needed.
|
||||||
|
|
||||||
|
For releases at or after `v1.0.0`:
|
||||||
|
|
||||||
|
- a major release may make intentional incompatible changes;
|
||||||
|
- a minor release may add backward-compatible behavior or raise the Go consumer
|
||||||
|
minimum as described below; and
|
||||||
|
- a patch release may make backward-compatible fixes.
|
||||||
|
|
||||||
|
Before `v1.0.0`, a minor release may change a documented public, operational,
|
||||||
|
configuration, data, integration, or compatibility contract when the release
|
||||||
|
note explains the impact and required consumer action. A patch release must
|
||||||
|
not intentionally break those contracts within its minor line.
|
||||||
|
|
||||||
|
Additional compatibility promises and deliberate exceptions belong in the
|
||||||
|
adopted policy or canonical contract documentation. Release notes explain
|
||||||
|
changes; they do not redefine current compatibility contracts.
|
||||||
|
|
||||||
|
## Consumer Minimum And Release Toolchain
|
||||||
|
|
||||||
|
Distinguish the minimum toolchain supported for consumers from the toolchain
|
||||||
|
maintainers use to build and validate a release. Record the consumer minimum in
|
||||||
|
the language's standard metadata and explain its support meaning in canonical
|
||||||
|
documentation. Define the selected release toolchain and its setup commands in
|
||||||
|
`docs/release.md`. Toolchain selection and upgrade cadence are repository-specific
|
||||||
|
choices.
|
||||||
|
|
||||||
|
For Go repositories, normally declare the initial stable release of the selected
|
||||||
|
series as the consumer minimum, such as `go 1.26.0`. Use a higher patch minimum
|
||||||
|
when a dependency or an identified correctness fix requires it, and document
|
||||||
|
the reason.
|
||||||
|
|
||||||
|
For release-policy purposes, successful build and release validation on the
|
||||||
|
selected Go patch release are sufficient to presume support across that Go
|
||||||
|
series, subject to the declared consumer minimum and concrete evidence of
|
||||||
|
incompatibility. Investigate specific compatibility concerns as they arise.
|
||||||
|
|
||||||
|
Use the selected release toolchain for candidate validation and release builds,
|
||||||
|
including CI jobs performing those tasks. A newer validation patch version alone
|
||||||
|
leaves the consumer minimum unchanged.
|
||||||
|
|
||||||
|
A minor release may raise the Go consumer minimum, including from one Go series
|
||||||
|
to the next, both before and after `v1.0.0`. This is an explicit exception to the
|
||||||
|
backward-compatibility requirement for minor releases. Patch releases preserve
|
||||||
|
the consumer minimum. Explain an increase's impact and required consumer action
|
||||||
|
in the release note. Other incompatible changes remain subject to the versioning
|
||||||
|
rules above.
|
||||||
|
|
||||||
|
## Release Documentation And Preparation
|
||||||
|
|
||||||
Every new release must include `docs/releases/<version>.md` in the tagged
|
Every new release must include `docs/releases/<version>.md` in the tagged
|
||||||
source, using the complete tag as the filename. Its title contains Promptkit
|
source tree. Use the complete version tag as the filename and begin with a
|
||||||
and that exact version. Use this structure:
|
title containing the product name and that exact version.
|
||||||
|
|
||||||
```markdown
|
Include a short summary and any relevant compatibility impact or upgrade
|
||||||
# Promptkit vMAJOR.MINOR.PATCH
|
instructions. Choose sections suited to the release and keep the note
|
||||||
|
proportionate to the change.
|
||||||
|
|
||||||
This release ...
|
Keep exact current contracts in their canonical documents and update affected
|
||||||
|
documentation with the behavior change. Link to those documents from the note
|
||||||
|
where useful. If annotated tags are used, derive their summary from the
|
||||||
|
checked-in note.
|
||||||
|
|
||||||
## Summary
|
Include the release footer defined by the [OSS policy](oss.md#discoverability)
|
||||||
|
in each new release document and preserve it on the hosted download page. The
|
||||||
|
release procedure defines the concrete URLs and publication mechanics. Prepare
|
||||||
|
these links with the release note; they do not add a network check or approval
|
||||||
|
gate.
|
||||||
|
|
||||||
## Compatibility
|
Handle version selection, release-note content, and retirement of completed
|
||||||
|
roadmaps during preparation. Preserve planned and deferred work and repair
|
||||||
|
affected links when retiring roadmaps. Reuse ordinary change review for
|
||||||
|
unintended files, credentials, and private data. Scope release-content review
|
||||||
|
to the tagged source.
|
||||||
|
|
||||||
## Upgrade
|
## Candidate Validation
|
||||||
|
|
||||||
## Changes
|
Provide one publication command that validates the final committed candidate,
|
||||||
```
|
checks publication state, and publishes the intended refs. A validation-only
|
||||||
|
mode or command may be provided for diagnosis, but must not be a required step
|
||||||
|
before publication repeats the same checks.
|
||||||
|
|
||||||
State “No action required” or “None” where appropriate rather than omitting a
|
Run the complete deterministic, offline default test suite once for the final
|
||||||
section. Keep exact current contracts in their canonical documents and link
|
candidate, following the [testing policy](testing.md). For repositories that
|
||||||
to them. Update affected canonical documentation in the same candidate.
|
publish builds, verify the required build targets. Select those targets for
|
||||||
|
the application's actual support commitments; native testing on each platform
|
||||||
|
and access to particular hardware are not default release prerequisites.
|
||||||
|
|
||||||
These notes are historical records, not optional disposable guidance. Published
|
Additional gates must address a concrete risk that the existing checks do not
|
||||||
release-note contents must not be replaced under the same version. Corrections
|
adequately cover. Do not automatically require linting, race testing, module
|
||||||
belong in a subsequent release. Annotated-tag messages complement the checked-in
|
hygiene audits, documentation audits, credential scans, example validation, or
|
||||||
note: generate the annotation from that note and the validated commit rather
|
expanded platform matrices solely because they are possible. Retain such
|
||||||
than maintaining a second summary.
|
checks when their distinct protection justifies their cost. A one-time migration
|
||||||
|
check must not silently become a permanent release requirement.
|
||||||
|
|
||||||
## Validation And Publication
|
Release tests must not require real credentials, mutable application services,
|
||||||
|
paid APIs, provider probes, or inference calls. The procedure may describe dependency
|
||||||
|
preparation separately from offline validation.
|
||||||
|
|
||||||
The [publication script](../../scripts/release.sh) runs the
|
Avoid duplicate validation across release phases. Successful checks for the
|
||||||
[local checks](../../scripts/release-check.sh) once for the final candidate.
|
same final candidate need not be repeated solely to satisfy another phase;
|
||||||
Retain clean-candidate, synchronized-main, unused-version, module-hygiene,
|
any reuse must clearly apply to that exact candidate. Persistent validation
|
||||||
release-note structure, and tag-target checks. Publish only the selected tag
|
caches are not required. If the candidate changes, validate the new candidate.
|
||||||
ref. Changing the candidate requires fresh validation. No provider endpoint
|
Cheap publication-state checks may be repeated immediately before pushing
|
||||||
probes or inference calls are release checks.
|
because local or remote state may have changed during validation.
|
||||||
|
|
||||||
Release-note content and retirement of completed roadmaps are preparation
|
Automate mechanical guards. Human review gates are strongly disfavored: add one
|
||||||
responsibilities, not separate human sign-off gates. Preserve planned and
|
only through an explicit project-specific policy decision explaining the
|
||||||
deferred work and repair links. Ordinary change review covers unintended files,
|
concrete risk and why ordinary change review or automation is insufficient.
|
||||||
secrets, and private data; do not repeat it as a release-specific approval.
|
Do not add routine confirmations for checks the command can perform itself.
|
||||||
|
|
||||||
A successful tag push to origin completes the release. Never wait for CI to
|
## Publication Guards
|
||||||
finish or succeed. CI results, remote read-back, and Go module availability
|
|
||||||
are informational follow-ups, not completion gates. An ambiguous push result
|
Automate the checks needed to publish the intended source under a fresh version:
|
||||||
requires confirming publication on origin, not waiting for CI.
|
|
||||||
|
- the version has the adopted format and is unused locally and on the
|
||||||
|
authoritative remote;
|
||||||
|
- the worktree and index are clean on the primary release branch;
|
||||||
|
- the source being published is the exact candidate that passed validation,
|
||||||
|
including its matching release note;
|
||||||
|
- the remote primary branch already identifies that candidate, or an atomic
|
||||||
|
source/tag push will advance it to that candidate without discarding remote
|
||||||
|
history; and
|
||||||
|
- the tag identifies the validated candidate and only the intended refs are
|
||||||
|
published.
|
||||||
|
|
||||||
|
Fetch the authoritative branch and tag state as needed for these checks.
|
||||||
|
Do not force branch or tag updates to bypass a failed guard. Publish explicit
|
||||||
|
refs rather than all local tags. Do not require CI success, publication-service
|
||||||
|
availability checks, or a separate manual confirmation for each guard.
|
||||||
|
|
||||||
## Immutability And Failure Recovery
|
## Immutability And Failure Recovery
|
||||||
|
|
||||||
Never move, replace, recreate, or delete a published tag or replace published
|
Published version tags and published contents are immutable. Do not move,
|
||||||
release contents. If validation fails before publication, correct the candidate,
|
overwrite, recreate, or delete a published tag, or replace a published artifact,
|
||||||
rerun the complete checks, and repeat the publication guards. Remove an
|
checksum, signature, provenance statement, or release note under that version.
|
||||||
unpublished local tag only after confirming it was not published.
|
The checked-in note at the immutable tag is that release's historical record.
|
||||||
|
|
||||||
If publication or subsequent investigation reveals a defect, leave the
|
If validation fails before publication, correct and commit the candidate and
|
||||||
published release intact. Correct the defect on main, select a new patch or
|
rerun the publication command. If a push fails or its outcome is ambiguous,
|
||||||
other semantically appropriate version, write its matching release note, and
|
inspect the authoritative remote before retrying. Remove an unpublished local
|
||||||
repeat the complete procedure. If publication status is uncertain, inspect
|
tag only after confirming it was never published or used by a release channel.
|
||||||
the authoritative remote before deciding whether a tag is unpublished; do not
|
An unavailable remote leaves publication status unknown; do not overwrite a tag
|
||||||
overwrite it to retry.
|
to resolve uncertainty.
|
||||||
|
|
||||||
Transient network, authentication, CI, or module-availability failures do not
|
Transient network, authentication, CI, or consumer-availability failures do not
|
||||||
by themselves warrant a new version. Investigate separately and publish a
|
by themselves require a new version. Asynchronous delivery may be retried from
|
||||||
corrective release only when published contents actually need changing.
|
the same immutable tag. Missing artifacts may be published later, provided
|
||||||
|
existing published contents are not replaced and the additions remain consistent
|
||||||
|
with the tagged source and any already-published checksum manifest. If that
|
||||||
|
cannot be achieved, publish a new version.
|
||||||
|
|
||||||
## Policy Exceptions And Deliberate Scope
|
A defect requiring changes to published source or contents requires a corrected
|
||||||
|
commit, a new semantically appropriate version, a matching release note, and
|
||||||
|
the release procedure again. Leave the original release intact. A channel's
|
||||||
|
yank, deprecation, or advisory mechanism may warn consumers away from a defective
|
||||||
|
release without replacing its contents or reusing its version.
|
||||||
|
|
||||||
This adoption follows the maintainer-approved lean workflow rather than every
|
## Repository Release Procedure
|
||||||
base-policy guard:
|
|
||||||
|
|
||||||
- Collision checks concern origin, the authoritative publication destination,
|
Maintain `docs/release.md` as a short, executable maintainer runbook. It must
|
||||||
rather than every configured remote. Local and origin tag checks, synchronized
|
identify:
|
||||||
main, and an explicit selected-ref push protect the actual release channel.
|
|
||||||
- Automated local validation and publication-state checks replace manual
|
|
||||||
mechanical inspections and separate human sign-offs. Ordinary change review,
|
|
||||||
editorial responsibility for release notes, and roadmap housekeeping remain;
|
|
||||||
repeating them as approvals would add friction without distinct protection.
|
|
||||||
- Successful tag publication completes this source-only release. CI can be slow
|
|
||||||
or unreliable, and module availability depends on external services. Neither
|
|
||||||
is a completion gate. Local candidate validation and explicit tag-target
|
|
||||||
checks protect publication; ambiguous push results are resolved against origin.
|
|
||||||
Actual defects are corrected in a new release, never by replacing a tag.
|
|
||||||
|
|
||||||
Ignored local files are not prohibited merely because they exist: they are not
|
1. prerequisites, selected release toolchain, primary branch, and authoritative
|
||||||
included in the tagged commit. The clean-candidate guard and ordinary review of
|
remote;
|
||||||
tracked changes protect the published contents.
|
2. version and release-note preparation;
|
||||||
|
3. one publication command with its selected local checks and automated guards;
|
||||||
|
4. declared deliverables, required build targets, and automation responsibilities;
|
||||||
|
5. the successful-push completion point and separately reported delivery status;
|
||||||
|
and
|
||||||
|
6. failure handling, ambiguous-push recovery, and asynchronous delivery retries.
|
||||||
|
|
||||||
The catalog profile provision preserves an already approved compatibility
|
Use copyable commands where practical. Keep optional diagnostics and consumer
|
||||||
boundary; it does not extend volatility to backend definitions or runtime
|
verification clearly optional. The procedure must not turn preparation work,
|
||||||
contracts. Further release-governance changes require deliberate approval.
|
ordinary change review, or post-publication investigation into additional
|
||||||
|
release gates.
|
||||||
|
|
||||||
|
## Promptkit Release Configuration
|
||||||
|
|
||||||
|
The release-note requirements were adopted on 2026-09-23 and licensing
|
||||||
|
navigation on 2026-09-26. Apply them prospectively; previously published notes
|
||||||
|
and tags retain their original records.
|
||||||
|
|
||||||
|
Promptkit publishes source-only Go modules from main to origin on Gitea, using
|
||||||
|
annotated stable SemVer tags. The source commit is pushed through main before
|
||||||
|
the tag. After source publication, maintainers use ReleaseKit locally to deliver
|
||||||
|
a Gitea release page containing the complete checked-in note, including its
|
||||||
|
licensing footer. There are no binaries or other deliverables. Source publication
|
||||||
|
and hosted delivery have separate outcomes; delivery credentials and service
|
||||||
|
availability do not gate source-release completion. The
|
||||||
|
[release procedure](../release.md) owns commands and recovery.
|
||||||
|
|
||||||
|
Release preparation uses the latest Go installation available in the development
|
||||||
|
environment to build the maintainer release executable and validate the
|
||||||
|
candidate, including the consumer examples. Keep that same local toolchain
|
||||||
|
active throughout those operations.
|
||||||
|
|
||||||
|
For a minor release, advance Promptkit's consumer minimum to the initial stable
|
||||||
|
release of that Go series, retaining a higher patch minimum when required by a
|
||||||
|
dependency or an identified correctness fix. Patch releases preserve the
|
||||||
|
existing consumer minimum. Commit any module update before validating the final
|
||||||
|
candidate. The [release procedure](../release.md#prepare-the-candidate) owns
|
||||||
|
setup commands, and the consumer guide explains
|
||||||
|
[Go compatibility](../consumers/pkg-promptkit.md#go-compatibility).
|
||||||
|
|
||||||
|
Use a title containing `Promptkit` and the exact version, followed by a short
|
||||||
|
summary and any relevant compatibility impact or upgrade instructions. The
|
||||||
|
note is committed with the candidate and supplies the annotated tag message.
|
||||||
|
No fixed set of section headings is required.
|
||||||
|
|
||||||
|
The [publication command](../../scripts/release.sh) delegates to the released
|
||||||
|
ReleaseKit tool selected in `go.mod`. The committed
|
||||||
|
[profile](../../releasekit.json) selects the race-enabled Go suite, full vet,
|
||||||
|
committed Go formatting, local Markdown links, workspace and filesystem
|
||||||
|
replacement inspection. The Go suite includes the offline semantic checks for
|
||||||
|
the three maintained consumer examples. It also rejects the configured local
|
||||||
|
delivery token path if committed; this narrowly scoped check is not a general credential
|
||||||
|
scan. These are the selected checks for the final candidate; a separate
|
||||||
|
ordinary test run or standalone build is not an
|
||||||
|
additional release gate. The upstream tool owns candidate, remote, and tag
|
||||||
|
publication checks. It returns native status 0 for confirmed publication, 1
|
||||||
|
for a known validation or publication failure, 2 for invalid input or profile,
|
||||||
|
and 3 when the push outcome is unknown. For status 3, resolve the exact
|
||||||
|
captured publication endpoint before retrying; the repository-specific
|
||||||
|
[procedure](../release.md#failure-recovery) links to the upstream recovery
|
||||||
|
instructions. Do not wait for CI or Go module availability. Licensing footer
|
||||||
|
URLs are preparation work, not a network gate.
|
||||||
|
|
||||||
|
Hosted delivery is a separate post-publication step for new releases. It reuses
|
||||||
|
the immutable tag and note without rerunning candidate validation or waiting
|
||||||
|
for CI. Retry failed delivery against that same version after resolving any
|
||||||
|
uncertain hosted write; preserve published tags and hosted content. Report
|
||||||
|
delivery status separately from the completed source release.
|
||||||
|
|||||||
@@ -1,43 +1,32 @@
|
|||||||
# Testing Policy
|
# Testing Policy
|
||||||
|
|
||||||
**Base Policy Version:** 1.0
|
**Base Policy Version:** 1.3
|
||||||
|
|
||||||
**Last Updated:** 2026-09-23
|
**Last Updated**: 2026-09-27
|
||||||
|
|
||||||
|
**Upstream Source:** [testing.md](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/testing.md)
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
Our tests exist to make **incorrect changes expensive and correct changes cheap**.
|
Our tests exist to make **incorrect changes expensive and correct changes cheap**.
|
||||||
|
|
||||||
We do not optimize for test count, line coverage, exhaustive isolation, or the fewest possible tests. We optimize for sufficient confidence in important behavior while imposing as little unnecessary friction as possible on future development.
|
We optimize for sufficient confidence in important behavior while minimizing friction on future development.
|
||||||
|
|
||||||
## Every test has a cost
|
## Every test has a cost
|
||||||
|
|
||||||
Testing is not an unqualified good. Every test imposes both an immediate cost and a continuing lifetime cost.
|
Every test has an immediate and continuing cost: writing, review, execution,
|
||||||
|
diagnosis, fixture maintenance, and updates when legitimate behavior changes.
|
||||||
A test must be:
|
Tests can also constrain refactoring, duplicate policy, and add failure noise.
|
||||||
|
|
||||||
- written and reviewed;
|
|
||||||
- understood by future maintainers and coding agents;
|
|
||||||
- executed in maintainer-run validation;
|
|
||||||
- diagnosed when it fails;
|
|
||||||
- updated when legitimate behavior changes;
|
|
||||||
- maintained as fixtures, APIs, and dependencies evolve; and
|
|
||||||
- removed or rewritten when it becomes redundant, brittle, misleading, or obsolete.
|
|
||||||
|
|
||||||
Tests also create cognitive and architectural friction. They can constrain refactoring, duplicate policy, slow feedback loops, add noise to failures, and cause harmless implementation changes to require unrelated edits across the suite.
|
|
||||||
|
|
||||||
A test is warranted only when the confidence it provides justifies these costs.
|
A test is warranted only when the confidence it provides justifies these costs.
|
||||||
|
Evaluate that tradeoff at two levels:
|
||||||
Apply this cost-benefit analysis at two levels:
|
|
||||||
|
|
||||||
1. **Per test:** What realistic defect does this test detect, how consequential would that defect be, and is that protection worth the test's lifetime cost?
|
1. **Per test:** What realistic defect does this test detect, how consequential would that defect be, and is that protection worth the test's lifetime cost?
|
||||||
2. **Across the suite:** Does this collection provide materially more confidence than a smaller, simpler suite would?
|
2. **Across the suite:** Does this collection provide materially more confidence than a smaller, simpler suite would?
|
||||||
|
|
||||||
The preferred test suite is a **lean suite that provides sufficient confidence in the risks that matter, without redundant or low-value tests**. We seek sufficient confidence with the least unnecessary testing friction, not the fewest possible tests.
|
Some friction is intentional. Tests should make dangerous changes—such as breaking compatibility, corrupting data, violating security boundaries, or reintroducing subtle bugs—require deliberate review. Keep ordinary internal changes inexpensive.
|
||||||
|
|
||||||
Some friction is intentional. Tests should make dangerous changes—such as breaking compatibility, corrupting data, violating security boundaries, or reintroducing subtle bugs—require deliberate review. They should not make ordinary internal changes needlessly expensive.
|
Evaluate maintenance cost against the specific failure being protected. When omitting a plausible test, be able to state why the protected failure is low-risk, already covered, obvious, reversible, or cheaper to detect elsewhere. For consequential, subtle, or difficult-to-observe behavior, the presumption should favor testing.
|
||||||
|
|
||||||
The cost of a test is not a reason to omit testing by default. Do not cite maintenance cost abstractly. When omitting a plausible test, be able to state why the protected failure is low-risk, already covered, obvious, reversible, or cheaper to detect elsewhere. For consequential, subtle, or difficult-to-observe behavior, the presumption should favor testing.
|
|
||||||
|
|
||||||
## Default testing style
|
## Default testing style
|
||||||
|
|
||||||
@@ -51,6 +40,12 @@ Use a **classical/Detroit-style** approach:
|
|||||||
|
|
||||||
Examples of appropriate seams include clocks, randomness, subprocesses, remote APIs, object storage, email, and paid LLM calls.
|
Examples of appropriate seams include clocks, randomness, subprocesses, remote APIs, object storage, email, and paid LLM calls.
|
||||||
|
|
||||||
|
## Promptkit Test Boundaries
|
||||||
|
|
||||||
|
Promptkit is a library without a CLI or hosted CI. The execution requirements,
|
||||||
|
facade test ownership, public error guidance, and offline consumer examples
|
||||||
|
below tailor the standard to that boundary.
|
||||||
|
|
||||||
## Test execution requirements
|
## Test execution requirements
|
||||||
|
|
||||||
Promptkit currently uses maintainer-run validation rather than hosted CI.
|
Promptkit currently uses maintainer-run validation rather than hosted CI.
|
||||||
@@ -63,17 +58,24 @@ and repository hygiene.
|
|||||||
Introducing hosted CI later would supplement, not silently redefine, this
|
Introducing hosted CI later would supplement, not silently redefine, this
|
||||||
documented validation model.
|
documented validation model.
|
||||||
|
|
||||||
Full release-candidate validation includes race-enabled package tests, explicit
|
The selected ReleaseKit profile runs the race-enabled package suite and full
|
||||||
static analysis, and execution with automated semantic checks of the three maintained
|
vet, checks committed Go formatting, local Markdown links, and module
|
||||||
offline consumer examples. The test run builds the packages; the examples
|
distribution hygiene. The Go suite includes semantic assertions for the three
|
||||||
also exercise ordinary compilation. Separate ordinary tests and a standalone
|
maintained offline consumer examples. ReleaseKit owns this shared validation
|
||||||
build are not additional release gates. Ordinary tests remain useful during
|
machinery and publication workflow. The test run builds the packages and
|
||||||
development. The preparation example protects assembled preparation and
|
exercises the examples. Separate ordinary tests and a
|
||||||
|
standalone build are not additional release gates. Ordinary tests remain useful
|
||||||
|
during development. The preparation example protects assembled preparation and
|
||||||
inspection behavior. The execution example separately protects assembled
|
inspection behavior. The execution example separately protects assembled
|
||||||
`Run`, injected-client, validation, usage, and result behavior. The observation
|
`Run`, injected-client, validation, usage, and result behavior. The observation
|
||||||
example protects assembled built-in retries, structured proposal and assessment
|
example protects assembled built-in retries, structured proposal and assessment
|
||||||
runs, and application-owned interpretation of metadata.
|
runs, and application-owned interpretation of metadata.
|
||||||
|
|
||||||
|
The standalone `python3 scripts/test-release.py` command exercises Promptkit's
|
||||||
|
ReleaseKit wiring against disposable local repositories. It is focused
|
||||||
|
development validation when release tooling changes, not part of the default
|
||||||
|
Go test suite or a recurring release check.
|
||||||
|
|
||||||
Tests in the default suite must be deterministic, offline, and independent of
|
Tests in the default suite must be deterministic, offline, and independent of
|
||||||
real credentials. They must not invoke paid APIs, use live network
|
real credentials. They must not invoke paid APIs, use live network
|
||||||
dependencies, or depend on mutable external services. Tests that require live
|
dependencies, or depend on mutable external services. Tests that require live
|
||||||
@@ -110,7 +112,7 @@ Use each test type where it protects a distinct risk:
|
|||||||
|
|
||||||
## What deserves tests
|
## What deserves tests
|
||||||
|
|
||||||
Prioritize tests for:
|
To the extent the following features are present in the application, prioritize tests for:
|
||||||
|
|
||||||
1. Public and package-level contracts.
|
1. Public and package-level contracts.
|
||||||
2. Domain rules and important invariants.
|
2. Domain rules and important invariants.
|
||||||
@@ -118,17 +120,29 @@ Prioritize tests for:
|
|||||||
4. Failure handling, cancellation, retries, recovery, and partial success.
|
4. Failure handling, cancellation, retries, recovery, and partial success.
|
||||||
5. Serialization, schemas, compatibility, and round trips.
|
5. Serialization, schemas, compatibility, and round trips.
|
||||||
6. Previously observed or plausible regressions.
|
6. Previously observed or plausible regressions.
|
||||||
7. Representative integration and consumer workflows.
|
7. Representative integration and end-to-end workflows.
|
||||||
|
|
||||||
A package-level contract is behavior relied upon by another package or major collaborator, not every observable detail of a package implementation.
|
A package-level contract comprises behavior relied upon by another package or major collaborator.
|
||||||
|
|
||||||
For behavior involving **data integrity, destructive operations, compatibility, security, concurrency, idempotency, or recovery**, presume that durable tests are required unless the behavior is already credibly protected at another layer.
|
Choose tests for requested behavior and established contracts. Tests must not
|
||||||
|
introduce new restrictions, guarantees, or threat scenarios merely because they
|
||||||
|
could be asserted. Follow the [agent policy](agents.md) when deciding whether a
|
||||||
|
behavior belongs to the contract; current implementation behavior alone is not
|
||||||
|
sufficient authority.
|
||||||
|
|
||||||
|
For established behavior involving **data integrity, destructive operations,
|
||||||
|
compatibility, security, concurrency, idempotency, or recovery**, favor durable
|
||||||
|
tests for realistic failures whose consequences warrant protection. Identify
|
||||||
|
the failure and the existing contract it would violate. These categories call
|
||||||
|
for attention, not an automatic test inventory or permission to strengthen the
|
||||||
|
application's guarantees. Account for protection already provided at other layers.
|
||||||
|
|
||||||
Do not add tests merely because a function, branch, or line exists. Do not add a test when the same meaningful risk is already adequately protected elsewhere.
|
Do not add tests merely because a function, branch, or line exists. Do not add a test when the same meaningful risk is already adequately protected elsewhere.
|
||||||
|
|
||||||
## Choose the right test boundary
|
## Choose the right test boundary
|
||||||
|
|
||||||
Test through the narrowest stable boundary that expresses the behavior clearly.
|
Choose the narrowest stable boundary that expresses the behavior clearly and
|
||||||
|
provides durable confidence with minimal incidental coupling.
|
||||||
|
|
||||||
This is often the package API, but it may instead be:
|
This is often the package API, but it may instead be:
|
||||||
|
|
||||||
@@ -136,30 +150,15 @@ This is often the package API, but it may instead be:
|
|||||||
- a package-level operation when several internal collaborators jointly produce the behavior; or
|
- a package-level operation when several internal collaborators jointly produce the behavior; or
|
||||||
- a larger integration boundary when correctness emerges from interaction with a real dependency.
|
- a larger integration boundary when correctness emerges from interaction with a real dependency.
|
||||||
|
|
||||||
Do not force all behavior through oversized consumer-workflow tests. Do not
|
|
||||||
test every private helper merely because it exists. Choose the boundary that
|
|
||||||
gives durable confidence with the least incidental coupling.
|
|
||||||
|
|
||||||
## Test behavior, not implementation
|
## Test behavior, not implementation
|
||||||
|
|
||||||
A test should protect a decision, contract, or invariant—not memorialize the current implementation.
|
A test should protect a selected decision, contract, or invariant.
|
||||||
|
|
||||||
Before adding or retaining a test, ask:
|
A test is suspect when it primarily detects changes to private constants or
|
||||||
|
helpers, equivalent operation ordering, incidental formatting, equivalent
|
||||||
|
algorithms, or internal structure that preserves behavior.
|
||||||
|
|
||||||
> What realistic defect would this test catch?
|
Behavioral tests should normally remain stable through internal refactoring that preserves the contract.
|
||||||
|
|
||||||
A test is suspect when its main purpose is to detect that someone:
|
|
||||||
|
|
||||||
- changed an internal constant;
|
|
||||||
- renamed or split a private helper;
|
|
||||||
- reordered equivalent internal operations;
|
|
||||||
- changed incidental formatting;
|
|
||||||
- replaced one correct algorithm with another; or
|
|
||||||
- refactored internal object structure without changing behavior.
|
|
||||||
|
|
||||||
Refactoring should normally require no test edits unless the refactored structure is itself part of the contract.
|
|
||||||
|
|
||||||
A test can be factually correct and still have negative value. Accurately describing current behavior is not enough; the protected behavior must be important enough to justify the future friction.
|
|
||||||
|
|
||||||
## Expected effects of different changes
|
## Expected effects of different changes
|
||||||
|
|
||||||
@@ -176,13 +175,13 @@ A test failing is not the same as a test needing to be edited. Many tests may co
|
|||||||
|
|
||||||
## Separate mechanism from policy
|
## Separate mechanism from policy
|
||||||
|
|
||||||
Configurable thresholds and defaults must not be duplicated throughout the test suite.
|
Derive expectations from test-controlled configuration and relationships.
|
||||||
|
|
||||||
The following fragments are illustrative rather than standalone Go programs.
|
For this example, assume the selected contract rejects a request to start more
|
||||||
Do not encode an internal concurrency limit indirectly:
|
processes than a configured limit. Do not encode its production default indirectly:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Production policy:
|
// Production default:
|
||||||
const maxConcurrency = 4
|
const maxConcurrency = 4
|
||||||
|
|
||||||
// Brittle test:
|
// Brittle test:
|
||||||
@@ -200,37 +199,40 @@ require.NoError(t, runner.Start(limit))
|
|||||||
require.ErrorIs(t, runner.Start(limit+1), ErrTooMuchConcurrency)
|
require.ErrorIs(t, runner.Start(limit+1), ErrTooMuchConcurrency)
|
||||||
```
|
```
|
||||||
|
|
||||||
The test should prove:
|
For this selected contract, the test should prove:
|
||||||
|
|
||||||
- the configured limit is accepted; and
|
- the configured limit is accepted; and
|
||||||
- one beyond the configured limit is rejected.
|
- one beyond the configured limit is rejected.
|
||||||
|
|
||||||
|
Rejection is not a general requirement for concurrency control. If the selected
|
||||||
|
contract queues excess work or limits active workers while accepting larger
|
||||||
|
batches, test that behavior instead. Do not add rejection or a configurable
|
||||||
|
limit solely to reproduce this example.
|
||||||
|
|
||||||
The production default should be tested exactly only when its literal value is itself a public, operational, safety, protocol, or compatibility requirement.
|
The production default should be tested exactly only when its literal value is itself a public, operational, safety, protocol, or compatibility requirement.
|
||||||
|
|
||||||
Apply the same rule to limits, timeouts, capacities, retry counts, and ranges: test relationships and behavior, not duplicated literals.
|
Apply the same rule to limits, timeouts, capacities, retry counts, and ranges: test relationships and behavior.
|
||||||
|
|
||||||
For concurrency limits, test both kinds of behavior when relevant:
|
For established concurrency limits, distinguish these behaviors where the contract requires them:
|
||||||
|
|
||||||
1. **Configuration enforcement:** invalid or excessive requested values are handled correctly.
|
1. **Configuration handling:** values are accepted or rejected according to the selected contract.
|
||||||
2. **Runtime enforcement:** observed peak concurrency never exceeds the configured limit.
|
2. **Runtime enforcement:** observed peak concurrency never exceeds the configured limit.
|
||||||
|
|
||||||
Use a test-controlled limit and measure the behavior relative to that limit. Do not merely assert today's default value.
|
Use a test-controlled limit and measure the behavior relative to that limit.
|
||||||
|
|
||||||
## Avoid semantic duplication across layers
|
## Assign each behavior a test owner
|
||||||
|
|
||||||
Each behavior should have a clear test owner.
|
Where these layers exist, assign ownership as follows:
|
||||||
|
|
||||||
- Parser tests own parsing cases.
|
- Parser tests own parsing cases.
|
||||||
- Validator tests own validation rules.
|
- Validator tests own validation rules.
|
||||||
- Domain tests own transformations and invariants.
|
- Domain tests own transformations and invariants.
|
||||||
- Boundary tests own external integration behavior.
|
- Adapter tests own external integration behavior.
|
||||||
- Orchestrator tests own coordination and failure propagation.
|
- Orchestrator tests own coordination and failure propagation.
|
||||||
- Consumer-workflow tests prove that representative assembled library use
|
- External-package root tests own the public Go facade contract.
|
||||||
works.
|
- Consumer-workflow tests prove that representative assembled library use works.
|
||||||
|
|
||||||
Higher-level tests should not repeat every lower-level case. A single intentional policy change should not require unrelated edits across many test files.
|
Give each layer its distinct responsibility and keep expectation updates localized to the contract being changed.
|
||||||
|
|
||||||
Tests that are individually reasonable may still be collectively redundant. Evaluate the marginal value of each additional test in light of the protection already provided by the rest of the suite.
|
|
||||||
|
|
||||||
## Use test doubles deliberately
|
## Use test doubles deliberately
|
||||||
|
|
||||||
@@ -251,111 +253,78 @@ Mocks are appropriate when the contract includes facts such as:
|
|||||||
- an expensive API is called no more than once; or
|
- an expensive API is called no more than once; or
|
||||||
- a security audit event is emitted.
|
- a security audit event is emitted.
|
||||||
|
|
||||||
Do not use mocks merely to isolate every object or reproduce the implementation's call graph.
|
|
||||||
|
|
||||||
## Go-specific guidance
|
## Go-specific guidance
|
||||||
|
|
||||||
Use:
|
Select techniques by behavior and risk:
|
||||||
|
|
||||||
- table-driven tests for meaningful behavioral categories and boundaries;
|
- table-driven tests for meaningful behavioral categories and boundaries;
|
||||||
- `t.TempDir()` for real filesystem behavior;
|
- `t.TempDir()` for real filesystem behavior;
|
||||||
- `httptest.Server` for realistic HTTP interactions;
|
- `httptest.Server` for realistic HTTP interactions;
|
||||||
- fuzz tests for parsers, normalization, path handling, and broad input spaces;
|
- fuzz tests when exploring broad input spaces, such as parsers or path handling, adds useful protection beyond representative cases;
|
||||||
- golden files only when the complete output is intentionally stable;
|
- golden files only when the complete output is intentionally stable;
|
||||||
- integration tests where correctness depends on component interaction; and
|
- integration tests where correctness depends on component interaction; and
|
||||||
- a small number of representative consumer-workflow tests.
|
- a small number of representative end-to-end tests when assembled workflows expose meaningful risks not adequately covered at narrower boundaries.
|
||||||
|
|
||||||
Avoid exact error-string assertions unless the wording is itself contractual. Prefer `errors.Is`, `errors.As`, typed errors, or structured error fields.
|
Prefer `errors.Is`, `errors.As`, typed errors, or structured error fields. Use exact error-wording assertions only when the wording is contractual.
|
||||||
|
|
||||||
At public API boundaries, prefer stable error identity, structured values, and
|
At public API boundaries, prefer stable error identity, structured values, and
|
||||||
the smallest semantic fragment needed to identify the failure. Do not snapshot
|
the smallest semantic fragment needed to identify the failure. Use complete
|
||||||
complete diagnostic wording unless it is contractual.
|
diagnostic snapshots only when the wording is contractual.
|
||||||
|
|
||||||
Golden-file updates must require an explicit local flag. Ordinary validation
|
Golden-file updates must require an explicit local flag. Ordinary validation runs must never update golden files automatically, and reviewers must inspect the semantic diff before accepting an update.
|
||||||
runs must never update golden files automatically, and maintainers must inspect
|
|
||||||
the semantic diff before accepting an update.
|
|
||||||
|
|
||||||
Keep tests readable and direct. Test helpers and fixture frameworks must earn their own maintenance cost; do not build elaborate test infrastructure for small or isolated needs.
|
Keep tests readable and direct. Size test helpers and fixture frameworks to the maintenance benefit they provide.
|
||||||
|
|
||||||
## Coverage
|
## Coverage
|
||||||
|
|
||||||
Coverage is a diagnostic, not a target.
|
Use coverage to locate untested critical branches and unexpectedly weak packages. Evaluate test quality through meaningful behavioral protection and choose coverage according to risk.
|
||||||
|
|
||||||
Use it to find untested critical branches and unexpectedly weak packages. Do not write low-value tests solely to increase a percentage, and do not infer test quality from coverage alone.
|
Pure domain logic will often warrant higher coverage than facade wiring or external adapters. Uneven coverage is acceptable when it reflects risk.
|
||||||
|
|
||||||
Pure domain logic will often warrant higher coverage than facade wiring or
|
|
||||||
external adapters. Uneven coverage is acceptable when it reflects risk.
|
|
||||||
|
|
||||||
Increasing coverage is valuable only when the newly covered behavior protects a meaningful risk at an acceptable cost.
|
|
||||||
|
|
||||||
## Regression tests
|
## Regression tests
|
||||||
|
|
||||||
A bug fix should normally include a regression test that fails before the fix and passes afterward.
|
A bug fix should normally include a regression test that fails before the fix and passes afterward.
|
||||||
|
|
||||||
Retain the test when the defect could realistically recur and its consequences justify the ongoing cost. Prefer the narrowest durable test of the violated contract or invariant; do not preserve accidental implementation details from the original bug.
|
Retain the test when the defect could realistically recur and its consequences justify the ongoing cost. Prefer the narrowest durable test of the violated contract or invariant.
|
||||||
|
|
||||||
Not every historical bug requires a permanent test. If the underlying design has made recurrence impossible, the test has become redundant, or a stronger invariant test now subsumes it, remove or consolidate it.
|
Remove or consolidate a test when the underlying design has made recurrence impossible, its protection is redundant, or a stronger invariant test subsumes it.
|
||||||
|
|
||||||
## Deleting or rewriting tests
|
## Deleting or rewriting tests
|
||||||
|
|
||||||
Tests are maintained code, not permanent historical artifacts.
|
Delete or rewrite tests when maintenance cost exceeds their continuing
|
||||||
|
protective value.
|
||||||
Delete or rewrite a test when its maintenance cost exceeds the confidence it provides.
|
|
||||||
|
|
||||||
Strong candidates include tests that:
|
Strong candidates include tests that:
|
||||||
|
|
||||||
- require updates after harmless internal changes;
|
- require updates after harmless internal changes;
|
||||||
- directly assert private constants without protecting a real contract;
|
- assert incidental private constants or snapshot large amounts of incidental output;
|
||||||
- duplicate the same policy across several layers;
|
|
||||||
- verify mock choreography rather than outcomes;
|
- verify mock choreography rather than outcomes;
|
||||||
- snapshot large amounts of incidental output;
|
- duplicate policy across layers, retest trivial helpers covered by package
|
||||||
- test trivial private helpers already exercised through stable package behavior;
|
behavior, or protect risks covered more effectively elsewhere;
|
||||||
- protect risks already covered more effectively elsewhere;
|
|
||||||
- are flaky, misleading, obsolete, or disproportionately expensive to diagnose; or
|
- are flaky, misleading, obsolete, or disproportionately expensive to diagnose; or
|
||||||
- no longer correspond to a plausible failure mode.
|
- no longer correspond to a plausible failure mode.
|
||||||
|
|
||||||
Several brittle tests may encode one genuine requirement. Replace them with one durable behavior-level or invariant test rather than preserving all of them.
|
Several brittle tests may encode one genuine requirement. Consolidate them into one durable behavior-level or invariant test.
|
||||||
|
|
||||||
Deleting a low-value test can improve the quality of the suite by reducing noise, maintenance burden, and friction around legitimate change.
|
|
||||||
|
|
||||||
## Reviewing a proposed test
|
## Reviewing a proposed test
|
||||||
|
|
||||||
Use the following questions when the value, boundary, or durability of a proposed test is not self-evident. Significant test additions should be reviewable against them, but written answers are not required for every routine test.
|
Use the following questions when a proposed test needs closer evaluation. Significant test additions should be reviewable against them; written answers are optional for routine tests.
|
||||||
|
|
||||||
1. What realistic defect would it catch?
|
1. Which requested behavior or established contract does it protect, and what realistic defect would violate it?
|
||||||
2. How likely is that defect?
|
2. How likely and consequential is that defect, and is it already protected elsewhere?
|
||||||
3. How consequential would it be?
|
3. What is the narrowest stable boundary that can test it clearly?
|
||||||
4. Is the behavior already protected elsewhere?
|
4. Would a correct internal refactor or a change to an incidental default require test edits?
|
||||||
5. At which layer should this behavior be owned?
|
5. Does its confidence justify its maintenance, execution, and diagnostic cost, or would a smaller test suffice?
|
||||||
6. Does the test assert a durable contract or an incidental implementation detail?
|
|
||||||
7. Could the implementation be refactored without changing the behavior and without editing this test?
|
|
||||||
8. What should cause this test to fail?
|
|
||||||
9. What legitimate changes should not cause this test to fail?
|
|
||||||
10. What ongoing maintenance, execution, and diagnostic cost will the test impose?
|
|
||||||
11. Is there a smaller or more direct test that protects the same risk?
|
|
||||||
|
|
||||||
Do not add the test when its expected lifetime cost exceeds its expected protective value.
|
|
||||||
|
|
||||||
When deciding not to test plausible behavior, record or be able to explain why the risk is low, already protected, obvious, reversible, or cheaper to detect elsewhere.
|
|
||||||
|
|
||||||
## Definition of sufficient
|
## Definition of sufficient
|
||||||
|
|
||||||
A test suite is sufficient when:
|
A test suite is sufficient when:
|
||||||
|
|
||||||
- important contracts and invariants are protected;
|
- important requested behavior and established contracts are protected against realistic, consequential failures;
|
||||||
- meaningful boundaries and failure modes are exercised;
|
- boundaries and component interactions have coverage where they introduce meaningful risks not protected elsewhere;
|
||||||
- realistic and consequential regressions are credibly protected against silent recurrence;
|
- failures provide useful, distinct signals;
|
||||||
- behavior involving data integrity, destructive operations, compatibility, security, concurrency, idempotency, and recovery is credibly protected;
|
- tests usually remain stable through legitimate internal changes; and
|
||||||
- important external boundaries have realistic integration coverage;
|
|
||||||
- representative complete workflows are tested;
|
|
||||||
- failures provide useful signal rather than redundant noise;
|
|
||||||
- legitimate internal changes usually do not require test edits; and
|
|
||||||
- additional tests would mostly repeat existing protection or preserve inconsequential implementation details.
|
- additional tests would mostly repeat existing protection or preserve inconsequential implementation details.
|
||||||
|
|
||||||
Sufficiency is a risk judgment, not a coverage percentage or test count.
|
Reassess sufficiency as the application, its users, and the consequences of
|
||||||
Reassess it as the library, its consumers, and the consequences of failure
|
failure evolve.
|
||||||
evolve.
|
|
||||||
|
|
||||||
The governing rule is:
|
|
||||||
|
|
||||||
> Test heavily where failure is consequential, subtle, or difficult to detect after the fact. Test lightly where failure is obvious, reversible, and inexpensive—and retain no test whose lifetime cost exceeds the confidence it provides.
|
|
||||||
|
|||||||
185
docs/release.md
185
docs/release.md
@@ -1,82 +1,159 @@
|
|||||||
# Release Procedure
|
# Release Procedure
|
||||||
|
|
||||||
This runbook implements the [release policy](policy/release.md). A successful
|
This runbook implements the [release policy](policy/release.md). Promptkit
|
||||||
push of the selected annotated tag to origin completes the release. Never wait
|
publishes source-only Go module tags from `main` to `origin`, followed by a
|
||||||
for CI to finish or succeed.
|
page-only Gitea release. Successful publication of the intended tag, with its
|
||||||
|
source commit available on `main`, completes the source release. Report hosted
|
||||||
|
delivery separately. Do not wait for CI or Go module availability.
|
||||||
|
|
||||||
## Prepare The Candidate
|
## Prepare The Candidate
|
||||||
|
|
||||||
Required tools are Git, Go with race-detector support, POSIX shell, and Python
|
Required tools are Git, Go with race-detector support, and POSIX shell. Work
|
||||||
3. Publication requires push access to origin. Work from the repository root.
|
from the repository root with push access to `origin`.
|
||||||
|
|
||||||
Select an unused version and create `docs/releases/<version>.md` using the
|
Use the latest Go installation available locally for the release tool and its
|
||||||
[required structure](policy/release.md#release-documentation). Describe
|
child commands. Put it first on `PATH` and keep it selected throughout:
|
||||||
compatibility impact and consumer actions and update affected canonical docs.
|
|
||||||
For catalog updates, record selected catalog versions and any consumer action,
|
|
||||||
linking to the catalog policy rather than repeating its contracts.
|
|
||||||
|
|
||||||
Retire completed roadmaps, preserving planned or deferred work and repairing
|
```sh
|
||||||
links. This is housekeeping, not a separate approval gate. Review changes
|
export GOTOOLCHAIN=local
|
||||||
through the ordinary development workflow, commit them, and push main to
|
go version
|
||||||
origin. No repeated release-specific diff review is required.
|
```
|
||||||
|
|
||||||
|
For a minor release, normally advance the consumer minimum to the initial
|
||||||
|
stable patch of that Go series:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
release_go_version=$(go env GOVERSION)
|
||||||
|
release_go_series=${release_go_version#go}
|
||||||
|
release_go_series=${release_go_series%.*}
|
||||||
|
go mod edit "-go=${release_go_series}.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Retain a higher patch minimum when required by a dependency or identified fix.
|
||||||
|
For a patch release, preserve the existing minimum. Explain minimum increases
|
||||||
|
under the [versioning policy](policy/release.md#versioning-and-compatibility).
|
||||||
|
Prepare dependency caches before committing the candidate:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
Choose an unused stable version and create `docs/releases/<version>.md` with a
|
||||||
|
title containing `Promptkit` and that exact version, a short summary, and any
|
||||||
|
compatibility impact or upgrade instructions. End the note with:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/vX.Y.Z/COPYRIGHT) | [LICENSE](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/vX.Y.Z/LICENSE) | [ATTRIBUTION](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/vX.Y.Z/docs/oss/attribution.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
Substitute the selected version and check the paths against the candidate;
|
||||||
|
future tag URLs need not resolve during preparation. Follow the
|
||||||
|
[OSS policy](policy/oss.md). Historical notes and tags remain unchanged.
|
||||||
|
|
||||||
|
Supply delivery credentials through `RELEASEKIT_GITEA_TOKEN` or the ignored
|
||||||
|
local file `.releasekit/gitea-token`, with owner-only file permissions. A
|
||||||
|
nonblank environment value takes precedence. Source publication neither reads
|
||||||
|
nor requires this token. The [profile](../releasekit.json) selects the token
|
||||||
|
path for committed-file protection; keep credentials out of commits and logs.
|
||||||
|
|
||||||
|
Retire completed roadmaps, preserve unfinished work, and repair affected links.
|
||||||
|
Review and commit the candidate on `main`, including any module updates, then
|
||||||
|
push `main` to `origin` before publishing its tag.
|
||||||
|
|
||||||
## Validate And Publish
|
## Validate And Publish
|
||||||
|
|
||||||
Run the following with the chosen unused version (replace the placeholder):
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./scripts/release.sh vMAJOR.MINOR.PATCH
|
./scripts/release.sh vMAJOR.MINOR.PATCH
|
||||||
```
|
```
|
||||||
|
|
||||||
The [publication script](../scripts/release.sh) checks a clean, synchronized
|
The launcher uses `go tool` to build or reuse the module-selected ReleaseKit
|
||||||
main candidate, the version and unused tag, and the release note's title and
|
executable in Go's cache, using the selected local toolchain. ReleaseKit runs
|
||||||
required headings. It runs the [local checks](../scripts/release-check.sh)
|
the [profile's checks](../releasekit.json) once, verifies publication state, and
|
||||||
once, rechecks candidate identity and publication state, generates the
|
publishes only the intended tag. Preserve its native exit status and diagnostic.
|
||||||
annotation from the committed note plus the validated commit, and pushes only
|
Successful publication needs no separate remote read-back or CI wait.
|
||||||
the selected annotated tag.
|
|
||||||
|
|
||||||
There is no separate tag-message authoring, example-output inspection,
|
For optional local diagnosis, use `./scripts/releasekit.sh check VERSION`.
|
||||||
metadata inspection, or human sign-off. Compatibility and release-note content
|
It is not a required step before publication. The upstream
|
||||||
remain ordinary editorial responsibilities. Development validation is useful
|
[candidate-check guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/consumers/check.md)
|
||||||
but does not require a second full run before invoking the publication script.
|
and [CLI contract](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/cli.md)
|
||||||
|
own shared checks and exit-status definitions.
|
||||||
|
|
||||||
The script stops successfully after the tag push. CI completion, CI success,
|
## Deliver The Hosted Release
|
||||||
remote read-back, and Go module availability are not release completion gates.
|
|
||||||
Do not wait or poll for them. Provider probes and inference calls are never
|
|
||||||
part of release validation.
|
|
||||||
|
|
||||||
## Catalog Profile Compatibility
|
After source publication, keep a clean checkout at the published tag's commit:
|
||||||
|
|
||||||
The [release policy](policy/release.md#catalog-profile-compatibility) owns the
|
```sh
|
||||||
catalog compatibility boundary. This heading preserves older incoming links.
|
./scripts/releasekit.sh deliver vMAJOR.MINOR.PATCH
|
||||||
|
```
|
||||||
|
|
||||||
|
The profile selects page-only Gitea delivery for this repository. The hosted
|
||||||
|
body is the complete checked-in note, including its licensing footer. There are
|
||||||
|
no binary assets, checksum manifests, candidate revalidation, or CI waits.
|
||||||
|
ReleaseKit checks delivery credentials during delivery, not as a publication
|
||||||
|
gate. Report the hosted URL on success; delivery failure leaves the source
|
||||||
|
release complete.
|
||||||
|
|
||||||
|
For a later retry, use a clean checkout of the original tag with its recorded
|
||||||
|
module files and supply the token there. In a separate checkout, for example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
release_version=vMAJOR.MINOR.PATCH
|
||||||
|
release_tag_ref="refs/tags/$release_version"
|
||||||
|
git fetch origin "$release_tag_ref:$release_tag_ref"
|
||||||
|
release_tag_commit=$(git rev-parse --verify "$release_tag_ref^{commit}")
|
||||||
|
git switch --detach "$release_tag_commit"
|
||||||
|
release_worktree_status=$(git status --porcelain --untracked-files=all)
|
||||||
|
if [ -n "$release_worktree_status" ]; then
|
||||||
|
printf '%s\n' 'Delivery retry requires a clean tagged checkout.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
./scripts/releasekit.sh deliver "$release_version"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolve uncertain hosted writes before retrying the same immutable version.
|
||||||
|
Matching content is accepted; conflicts are not overwritten. Do not rerun
|
||||||
|
`publish` or move tags to repair delivery. Follow the upstream
|
||||||
|
[delivery guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/consumers/delivery.md)
|
||||||
|
for result interpretation and recovery. These commands apply to tags containing
|
||||||
|
this delivery configuration; older releases retain their original workflows.
|
||||||
|
|
||||||
## Failure Recovery
|
## Failure Recovery
|
||||||
|
|
||||||
If local checks fail, fix the candidate, commit and push changes, then rerun
|
For source-publication failures, use ReleaseKit's status and diagnostic to
|
||||||
the publication command. It never automatically removes a tag.
|
distinguish a candidate defect from a publication conflict. Correct and commit
|
||||||
|
candidate defects, push `main`, and rerun publication as appropriate. A failure
|
||||||
|
after local tag creation may leave a ref even when its object ID is unavailable.
|
||||||
|
|
||||||
If a push fails or its result is ambiguous, set `RELEASE_VERSION` to the
|
Status 3 means the push outcome remains unknown. Do not retry or remove the local
|
||||||
attempted version and inspect origin before retrying:
|
tag until the authoritative outcome is resolved. ReleaseKit attempts to save
|
||||||
|
private publication evidence and reports a record path or a separate recording
|
||||||
|
warning. A recording failure does not change the publication outcome.
|
||||||
|
|
||||||
|
Use the recovery command reported for that attempt, or discover local records
|
||||||
|
and explicitly select the matching attempt:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git ls-remote --tags origin "refs/tags/$RELEASE_VERSION" "refs/tags/$RELEASE_VERSION^{}"
|
./scripts/releasekit.sh publication-attempts
|
||||||
git rev-parse "refs/tags/$RELEASE_VERSION" "refs/tags/$RELEASE_VERSION^{}"
|
./scripts/releasekit.sh reconcile --record /absolute/path/to/selected-record.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Matching remote tag and peeled commit values confirm publication, even if the
|
Discovery is local; reconciliation observes the original destination without
|
||||||
push command reported an error. If the remote query fails, publication status
|
rerunning validation, changing tags, or delivering hosted content. Neither is
|
||||||
is still unknown. Do not delete or recreate the local tag until absence on
|
a routine gate after successful publication. Follow the upstream
|
||||||
origin is confirmed. Once confirmed unpublished, an unsuccessful attempt's
|
[recovery procedure](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/release.md#resolve-publication-failures)
|
||||||
local tag can be removed and the publication command rerun. Never overwrite
|
for outcomes, incomplete evidence, and older attempts without records.
|
||||||
a published tag.
|
|
||||||
|
|
||||||
CI or module-resolution problems may be investigated separately; temporary
|
Records are stored under Git's common directory by default and can contain
|
||||||
service, network, or authentication failures do not require another release.
|
credential-bearing endpoints. Keep them private, out of source control and
|
||||||
Only an actual defect requiring changes to published contents warrants a new
|
shared logs; retain them while needed for recovery and remove them when that
|
||||||
appropriate version. See the [recovery policy](policy/release.md#immutability-and-failure-recovery).
|
value expires. See the [evidence guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/consumers/publish.md#retain-and-reconcile-publication-evidence)
|
||||||
|
for storage and retention details.
|
||||||
|
|
||||||
## Policy Changes
|
Current absence does not prove a version was never published. Remove a retained
|
||||||
|
local tag only after establishing absence and confirming it was not already
|
||||||
Update the policy and this procedure together when changing release governance.
|
published or used by another release channel. Never reuse a known published
|
||||||
Consumers own their dependency-availability and release checks; their readiness
|
version or replace a published tag or hosted page. Corrections require a new
|
||||||
does not delay completion of a Promptkit release.
|
appropriate version.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ the existing Go API. This refactor requires no consumer code changes.
|
|||||||
|
|
||||||
This release selects OpenRouter `v2.20260922.1` and Rakestrawhome
|
This release selects OpenRouter `v2.20260922.1` and Rakestrawhome
|
||||||
`v2.20260922.0`; their exact inventories are linked from the
|
`v2.20260922.0`; their exact inventories are linked from the
|
||||||
[format reference](../formats.md#external-profile-inventories).
|
[migration identity mapping](../consumers/configuration-migration.md#identity-mapping).
|
||||||
|
|
||||||
The maintained names provide continuity only within the catalog versions
|
The maintained names provide continuity only within the catalog versions
|
||||||
selected and tested by a Promptkit release and when consumers do not override
|
selected and tested by a Promptkit release and when consumers do not override
|
||||||
@@ -37,8 +37,8 @@ an available profile or supply their own complete definition; the new
|
|||||||
`deepseek-flash-latest` and `deepseek-pro-latest` profiles are not automatic
|
`deepseek-flash-latest` and `deepseek-pro-latest` profiles are not automatic
|
||||||
aliases and need not preserve the previous settings.
|
aliases and need not preserve the previous settings.
|
||||||
|
|
||||||
See the [release policy](../release.md#catalog-profile-compatibility) for the
|
See the [release policy](../policy/release.md#versioning-and-compatibility) for the
|
||||||
compatibility boundary, the [format reference](../formats.md#built-in-profile-catalog)
|
compatibility boundary, the [migration identity mapping](../consumers/configuration-migration.md#identity-mapping)
|
||||||
for current Promptkit-owned definitions and selected external inventories, and
|
for current resource mappings, and
|
||||||
the [consumer guide](../consumers/pkg-promptkit.md#choose-a-profile-ownership-model)
|
the [consumer guide](../consumers/pkg-promptkit.md#choose-a-profile-ownership-model)
|
||||||
for configuration choices.
|
for configuration choices.
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ they are not a complete billing ledger. See the
|
|||||||
|
|
||||||
Promptkit is now licensed under LGPL-3.0-or-later. Review the
|
Promptkit is now licensed under LGPL-3.0-or-later. Review the
|
||||||
[license summary](../../README.md#license), [LGPL text](../../LICENSE), and
|
[license summary](../../README.md#license), [LGPL text](../../LICENSE), and
|
||||||
[accompanying GPL text](../../COPYING) before adoption or distribution.
|
[accompanying GPL text](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.11.0/COPYING) before adoption or distribution.
|
||||||
Previously published releases and dependency licenses are unchanged.
|
Previously published releases and dependency licenses are unchanged.
|
||||||
|
|
||||||
## Upgrade
|
## Upgrade
|
||||||
@@ -72,5 +72,5 @@ for a structured proposal-and-assessment workflow.
|
|||||||
|
|
||||||
Backend catalog selections are unchanged from v0.10.0: OpenRouter
|
Backend catalog selections are unchanged from v0.10.0: OpenRouter
|
||||||
`v2.20260922.1` and Rakestrawhome `v2.20260922.0`. See the
|
`v2.20260922.1` and Rakestrawhome `v2.20260922.0`. See the
|
||||||
[catalog compatibility policy](../policy/release.md#catalog-profile-compatibility)
|
[release compatibility policy](../policy/release.md#versioning-and-compatibility)
|
||||||
and [profile inventories](../formats.md#external-profile-inventories).
|
and [migration identity mapping](../consumers/configuration-migration.md#identity-mapping).
|
||||||
|
|||||||
43
docs/releases/v0.11.1.md
Normal file
43
docs/releases/v0.11.1.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Promptkit v0.11.1
|
||||||
|
|
||||||
|
This patch updates the maintained catalogs to MIT-licensed releases and
|
||||||
|
improves licensing documentation.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Both OpenRouter and Rakestrawhome catalogs now use `v2.20260926.0`. These
|
||||||
|
catalog releases change licensing and documentation only; their profiles,
|
||||||
|
backend definitions, and Go code are unchanged.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
No public API, runtime behavior, or profile changes are introduced.
|
||||||
|
Promptkit remains LGPL-3.0-or-later; the catalogs are now MIT-licensed.
|
||||||
|
See the [project grant](../../COPYRIGHT),
|
||||||
|
[component attribution](../oss/attribution.md), and
|
||||||
|
[release compatibility policy](../policy/release.md#versioning-and-compatibility).
|
||||||
|
|
||||||
|
## Upgrade
|
||||||
|
|
||||||
|
Update the module dependency:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gitea.maximumdirect.net/eric/promptkit@v0.11.1
|
||||||
|
```
|
||||||
|
|
||||||
|
No code or configuration migration is required from v0.11.0.
|
||||||
|
Consumers upgrading from earlier versions should also read the
|
||||||
|
[v0.11.0 migration guide](v0.11.0.md).
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Adopt the MIT-licensed catalog releases and update inventory links.
|
||||||
|
- Add centralized attribution and dependency license texts.
|
||||||
|
- Separate the project grant into COPYRIGHT and consolidate the LGPLv3 and
|
||||||
|
incorporated GPLv3 texts in LICENSE, replacing the separate COPYING file.
|
||||||
|
- Align documentation ownership and future release navigation with the
|
||||||
|
[OSS policy](../policy/oss.md), without adding release gates.
|
||||||
|
- Repair the historical v0.11.0 GPL-text link and retire the completed
|
||||||
|
licensing roadmap.
|
||||||
|
|
||||||
|
[COPYRIGHT](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.11.1/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.11.1/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.11.1/docs/oss/attribution.md)
|
||||||
38
docs/releases/v0.11.2.md
Normal file
38
docs/releases/v0.11.2.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Promptkit v0.11.2
|
||||||
|
|
||||||
|
This patch aligns repository policies with the current standard and clarifies
|
||||||
|
licensing and attribution for the copied policy documents.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The update adds policy provenance, selective contributor reading guidance, and
|
||||||
|
consistent upstream policy language while preserving Promptkit-specific rules.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
No public API, runtime behavior, dependency, or catalog changes are introduced.
|
||||||
|
Promptkit remains LGPL-3.0-or-later. The [project grant](../../COPYRIGHT)
|
||||||
|
identifies the separate CC-BY-SA-4.0 scope for copied or adapted policy material;
|
||||||
|
the [attribution notices](../oss/attribution.md) identify the affected documents.
|
||||||
|
The existing release checks and successful-tag-push completion point are unchanged.
|
||||||
|
|
||||||
|
## Upgrade
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gitea.maximumdirect.net/eric/promptkit@v0.11.2
|
||||||
|
```
|
||||||
|
|
||||||
|
No code or configuration migration is required from v0.11.1.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Align adopted policies with Standard Repository Policies while retaining
|
||||||
|
library-specific requirements and deliberate local decisions.
|
||||||
|
- Add per-document upstream source links and the
|
||||||
|
[policy-set provenance document](../policy/meta.md).
|
||||||
|
- Add selective policy reading to the [contributor guide](../development.md)
|
||||||
|
and update agent orientation.
|
||||||
|
- Standardize factual attribution entries and identify copied-policy licensing.
|
||||||
|
- Retire the completed policy-alignment roadmap.
|
||||||
|
|
||||||
|
[COPYRIGHT](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.11.2/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.11.2/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.11.2/docs/oss/attribution.md)
|
||||||
74
docs/releases/v0.12.0.md
Normal file
74
docs/releases/v0.12.0.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Promptkit v0.12.0
|
||||||
|
|
||||||
|
This release replaces Promptkit's configuration loader with ConfigKit and
|
||||||
|
migrates maintained catalogs to keyed `/v3` assets. It intentionally changes
|
||||||
|
the public construction API and configuration format.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Backends, profiles, and versioned prompts now share one layered configuration
|
||||||
|
snapshot built when `NewEngine` is called. ConfigKit supplies composition,
|
||||||
|
inheritance, presence, and source history; Promptkit validates the final
|
||||||
|
effective definitions and captures prompt content and templates. Consumers can
|
||||||
|
inspect the captured configuration explicitly without triggering a reload.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
This is an incompatible minor release under Promptkit's pre-1.0
|
||||||
|
[versioning policy](../policy/release.md#versioning-and-compatibility). The
|
||||||
|
former source options, `Config.PromptDir`, `Config.ProfileDir`, and the old
|
||||||
|
profile/backend convenience values have been removed. Configuration documents
|
||||||
|
now use literal keys beneath `backends`, `profiles`, and `prompts`; embedded
|
||||||
|
`id`/`version` fields and `base_profile` are no longer accepted. Source groups
|
||||||
|
compose strictly within a group and overlay in the supplied order. Explicit
|
||||||
|
zero, false, empty, and null values can now differ from omission; maps overlay
|
||||||
|
recursively, and an empty map does not clear an inherited map.
|
||||||
|
|
||||||
|
All final definitions, including unused ones, are validated during engine
|
||||||
|
construction. Prompt text and static templates are captured then; an existing
|
||||||
|
engine does not automatically reload changed configuration or message files.
|
||||||
|
Request artifacts, schemas, credentials, and provider operations retain their
|
||||||
|
runtime boundaries. See the [format reference](../formats.md) for the current
|
||||||
|
configuration contract and the [consumer migration guide](../consumers/pkg-promptkit.md#migrate-from-the-previous-configuration-api)
|
||||||
|
for API and source-selection changes.
|
||||||
|
|
||||||
|
Maintained OpenRouter and Rakestrawhome catalogs are selected as `/v3` modules,
|
||||||
|
both at `v3.20260930.0`. Their asset format changed; consumers that read catalog
|
||||||
|
assets directly must adopt the keyed format. The maintained Promptkit profile
|
||||||
|
IDs remain available under the [release compatibility policy](../policy/release.md#versioning-and-compatibility),
|
||||||
|
which does not promise fixed profile data.
|
||||||
|
|
||||||
|
## Upgrade
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gitea.maximumdirect.net/eric/promptkit@v0.12.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace removed source and backend registration options with
|
||||||
|
`Config.Sources`, explicitly ordered ConfigKit underlays, and optionally a
|
||||||
|
highest-priority main input. Use `DirectoryInputs` or `FilesystemInputs` for
|
||||||
|
selected directories, `ProfileSource` and `BackendSource` for typed
|
||||||
|
programmatic values, or `configkit.Mapping` for explicit null and combined
|
||||||
|
documents. Move profile `base_profile` to `extends` and embedded identities to
|
||||||
|
mapping keys. Review zero/false/null values and any assumptions that replacing
|
||||||
|
a model or backend discards unrelated explicit settings. Rebuild the engine
|
||||||
|
when definitions or referenced message files change. The
|
||||||
|
[consumer guide](../consumers/pkg-promptkit.md#migrate-from-the-previous-configuration-api)
|
||||||
|
provides the migration details and maintained examples.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Add ConfigKit v0.5.0 as the configuration boundary and consume released
|
||||||
|
OpenRouter and Rakestrawhome `/v3` catalog modules.
|
||||||
|
- Add keyed backend, profile, and prompt definitions with final-value domain
|
||||||
|
validation, explicit presence/null behavior, and source-relative content
|
||||||
|
capture.
|
||||||
|
- Add `ConfigurationSources`, directory discovery helpers, programmatic
|
||||||
|
profile/backend source builders, and read-only configuration inspection.
|
||||||
|
- Preserve execution workflows while rendering captured templates, applying
|
||||||
|
backend defaults after inheritance, and honoring explicit suppression of
|
||||||
|
provider settings.
|
||||||
|
- Remove superseded loader, catalog preflight, and inheritance machinery;
|
||||||
|
update examples, format guidance, and internal documentation.
|
||||||
|
|
||||||
|
[COPYRIGHT](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.12.0/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.12.0/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.12.0/docs/oss/attribution.md)
|
||||||
69
docs/releases/v0.13.0.md
Normal file
69
docs/releases/v0.13.0.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Promptkit v0.13.0
|
||||||
|
|
||||||
|
This release adds prompt requirement discovery and profile/prompt listing,
|
||||||
|
reduces avoidable configuration and execution allocations, and standardizes
|
||||||
|
maintainer publication through ReleaseKit. The minimum supported Go version
|
||||||
|
increases to 1.26.0.
|
||||||
|
|
||||||
|
## Discover Requirements And Available Definitions
|
||||||
|
|
||||||
|
- `Engine.InspectRequirements` reports recognizable input and caller-variable
|
||||||
|
dependencies for a selected prompt and supplied caller context before input
|
||||||
|
artifacts exist. It classifies dependencies as required, conditional, or
|
||||||
|
optional and reports structured limitations when discovery is incomplete.
|
||||||
|
Discovery coverage is not proof that requirements are satisfied or execution
|
||||||
|
will succeed. See the [requirement inspection guide](../consumers/pkg-promptkit.md#inspect-requirements-before-preparation)
|
||||||
|
and [GoDoc](../../requirements.go).
|
||||||
|
- `Engine.ListProfiles` returns effective profile IDs, and `Engine.ListPrompts`
|
||||||
|
returns exact ID/version pairs as `PromptIdentity` values. Lists come from the
|
||||||
|
engine's captured configuration, are lexically ordered and caller-owned, and
|
||||||
|
include consumer definitions and maintained profiles that remain effective.
|
||||||
|
Listing performs no provider or execution work. See the
|
||||||
|
[discovery guide](../consumers/pkg-promptkit.md#discover-available-resources-and-collections)
|
||||||
|
and [GoDoc](../../listing.go).
|
||||||
|
|
||||||
|
## Efficiency And Tooling
|
||||||
|
|
||||||
|
- Reduce response-sized copies in basic validation and rendered-message output,
|
||||||
|
and encode valid provider extra parameters only as part of the complete
|
||||||
|
request rather than serializing them twice.
|
||||||
|
- Use bounded initial file-size reservations without limiting accepted content,
|
||||||
|
releasing excess backing storage for small or empty completed artifacts.
|
||||||
|
- Reuse owned preparation values when creating prepared-execution snapshots,
|
||||||
|
while preserving independent inspection details and caller-owned data.
|
||||||
|
- Use lightweight captured profile identities and prompt metadata for inspection
|
||||||
|
instead of copying execution targets and definitions unnecessarily.
|
||||||
|
- Upgrade ConfigKit to v0.6.0 for more efficient loading, resolution, metadata,
|
||||||
|
and inspection processing. Configuration semantics remain unchanged; these
|
||||||
|
improvements primarily benefit construction and explicit operator inspection,
|
||||||
|
not upstream inference latency.
|
||||||
|
- Adopt module-selected ReleaseKit v0.3.0 for maintainer validation and tag
|
||||||
|
publication. The [release procedure](../release.md) owns the current commands
|
||||||
|
and recovery workflow; successful tag publication remains the completion
|
||||||
|
point, without waiting for CI.
|
||||||
|
|
||||||
|
These changes preserve existing execution and ownership contracts. Benefits
|
||||||
|
depend on workload; this release introduces no fixed performance guarantee.
|
||||||
|
|
||||||
|
## Compatibility And Upgrade
|
||||||
|
|
||||||
|
Consumers upgrading from v0.12.0 need **Go 1.26.0 or newer**, rather than the
|
||||||
|
previous 1.25.5 minimum. Update application build environments and any CI images
|
||||||
|
still using Go 1.25 before upgrading:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gitea.maximumdirect.net/eric/promptkit@v0.13.0
|
||||||
|
```
|
||||||
|
|
||||||
|
No application-code or configuration migration is required from v0.12.0 beyond
|
||||||
|
the Go toolchain update. The new discovery methods are optional additions;
|
||||||
|
existing construction, inspection, preparation, and execution workflows remain
|
||||||
|
available. See [Go compatibility](../consumers/pkg-promptkit.md#go-compatibility)
|
||||||
|
for the distinction between the consumer minimum and maintainer toolchain.
|
||||||
|
|
||||||
|
Maintainers should follow the current release runbook rather than the retired
|
||||||
|
`scripts/release-check.sh`. The publication entry point remains
|
||||||
|
`scripts/release.sh`; ReleaseKit now owns its shared validation and publication
|
||||||
|
mechanics.
|
||||||
|
|
||||||
|
[COPYRIGHT](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.13.0/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.13.0/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.13.0/docs/oss/attribution.md)
|
||||||
48
docs/releases/v0.14.0.md
Normal file
48
docs/releases/v0.14.0.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Promptkit v0.14.0
|
||||||
|
|
||||||
|
This release adds comparison of captured configuration inspections and updates
|
||||||
|
ConfigKit and the maintainer release tool. The minimum supported Go version
|
||||||
|
remains 1.26.0.
|
||||||
|
|
||||||
|
## Compare Captured Configurations
|
||||||
|
|
||||||
|
`ConfigurationInspection.Diff` compares the effective configuration captured by
|
||||||
|
two engines. Consumers can use it to review a candidate configuration, inspect
|
||||||
|
an overlay's effect, or present configuration changes to operators.
|
||||||
|
|
||||||
|
Comparison delegates to ConfigKit and returns its difference records, including
|
||||||
|
presence information, paired redaction, and protected-change indicators. It
|
||||||
|
does not reload sources or contact providers. Source-history differences alone
|
||||||
|
do not produce value differences.
|
||||||
|
|
||||||
|
An empty diff is not proof of equivalent execution: referenced file contents,
|
||||||
|
resolved credentials, later execution preparation, and request-specific
|
||||||
|
overrides are outside the comparison. Records can contain ordinary application
|
||||||
|
data; callers retain responsibility for their disclosure.
|
||||||
|
|
||||||
|
See the [configuration comparison guide](../consumers/pkg-promptkit.md#inspect-the-captured-configuration)
|
||||||
|
and [GoDoc](../../configuration_inspection.go) for usage and the exact contract.
|
||||||
|
|
||||||
|
## Dependency And Tooling Updates
|
||||||
|
|
||||||
|
- Upgrade ConfigKit from v0.6.0 to v0.7.1, providing the upstream comparison API.
|
||||||
|
v0.7.1 also updates ConfigKit's maintainer tooling without changing its runtime
|
||||||
|
behavior from v0.7.0.
|
||||||
|
- Upgrade the module-selected ReleaseKit tool from v0.3.0 to v0.4.1. Promptkit
|
||||||
|
retains its existing root-module profile, publication entry point, and release
|
||||||
|
gates. See the [release procedure](../release.md) for maintainer commands.
|
||||||
|
|
||||||
|
## Compatibility And Upgrade
|
||||||
|
|
||||||
|
No application-code, configuration, or Go-toolchain migration is required from
|
||||||
|
v0.13.0. Configuration comparison is an optional API addition; existing
|
||||||
|
construction, inspection, preparation, and execution workflows remain available.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gitea.maximumdirect.net/eric/promptkit@v0.14.0
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Go compatibility](../consumers/pkg-promptkit.md#go-compatibility) for the
|
||||||
|
distinction between the consumer minimum and maintainer toolchain.
|
||||||
|
|
||||||
|
[COPYRIGHT](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.14.0/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.14.0/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.14.0/docs/oss/attribution.md)
|
||||||
34
docs/releases/v0.15.0.md
Normal file
34
docs/releases/v0.15.0.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Promptkit v0.15.0
|
||||||
|
|
||||||
|
This release adds error-only validation of a concrete prompt request without
|
||||||
|
calling a model. The minimum supported Go version remains 1.26.0.
|
||||||
|
|
||||||
|
## Validate A Concrete Request
|
||||||
|
|
||||||
|
`Engine.ValidateRequest` accepts an ordinary `RunRequest` and returns nil when
|
||||||
|
it passes preparation, or the existing preparation error. Consumers can use it
|
||||||
|
for fail-fast checks, including downstream CLI validation commands, without
|
||||||
|
receiving a prepared snapshot.
|
||||||
|
|
||||||
|
The method shares `Prepare`'s request conversion and preparation path. Unlike
|
||||||
|
requirements inspection, it checks the actual supplied values and bindings.
|
||||||
|
Artifact and schema resource I/O still occurs; success does not certify provider
|
||||||
|
acceptance or later execution success. A subsequent run prepares independently.
|
||||||
|
Validation is optional, not a required step before preparation or execution.
|
||||||
|
|
||||||
|
See the [request validation guide](../consumers/pkg-promptkit.md#validate-a-concrete-request)
|
||||||
|
and [GoDoc](../../engine.go) for usage and the exact contract. Consumers that
|
||||||
|
need execution tied to a checked snapshot can use the existing
|
||||||
|
[prepared execution workflow](../consumers/pkg-promptkit.md#prepare-now-and-execute-the-same-snapshot-later).
|
||||||
|
|
||||||
|
## Compatibility And Upgrade
|
||||||
|
|
||||||
|
No application-code, configuration, or Go-toolchain migration is required from
|
||||||
|
v0.14.0. This is an optional API addition; existing inspection, preparation, and
|
||||||
|
execution workflows remain available.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gitea.maximumdirect.net/eric/promptkit@v0.15.0
|
||||||
|
```
|
||||||
|
|
||||||
|
[COPYRIGHT](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.15.0/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.15.0/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.15.0/docs/oss/attribution.md)
|
||||||
86
docs/releases/v0.16.0.md
Normal file
86
docs/releases/v0.16.0.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Promptkit v0.16.0
|
||||||
|
|
||||||
|
This release adds explicit smoke diagnostics, configuration collections,
|
||||||
|
namespace-scoped engine views, and expanded resource discovery. It also makes
|
||||||
|
intentional breaking changes to configuration ownership and prompt selection.
|
||||||
|
The minimum supported Go version remains 1.26.0.
|
||||||
|
|
||||||
|
## Configuration Collections And Discovery
|
||||||
|
|
||||||
|
Applications can compose caller-selected collections of backends, profiles,
|
||||||
|
and prompts. Collection resources have qualified identities such as
|
||||||
|
`support/answer`; root resources retain unqualified identities. `CollectionInput`
|
||||||
|
provides a convenience for mounting a selected ConfigKit input into a collection.
|
||||||
|
Promptkit now uses ConfigKit v0.8.0, including its optional source specification
|
||||||
|
header support.
|
||||||
|
|
||||||
|
`Engine.WithNamespaces` creates an immutable view with separate prompt, profile,
|
||||||
|
and backend scopes for short caller-supplied IDs. Views share the underlying
|
||||||
|
configuration, clients, capacity pools, and prepared-execution owner. Explicitly
|
||||||
|
qualified IDs and configured references keep their exact identities.
|
||||||
|
|
||||||
|
`ListProfiles` and `ListPrompts` support namespace filters. New `ListBackends`,
|
||||||
|
`ListNamespaces`, and `ListCollections` methods expose captured resource and
|
||||||
|
collection inventories. See the [collection format](../formats.md#source-headers-collections-and-resource-ids),
|
||||||
|
[scoped-view guide](../consumers/pkg-promptkit.md#use-scoped-engine-views), and
|
||||||
|
[discovery guide](../consumers/pkg-promptkit.md#discover-available-resources-and-collections).
|
||||||
|
|
||||||
|
## Explicit Smoke Diagnostics
|
||||||
|
|
||||||
|
`Engine.SmokeTest` provides backend reachability, authentication, text inference,
|
||||||
|
and structured-output inference diagnostics. Reports provide factual
|
||||||
|
observations, timing, and available usage rather than an overall readiness
|
||||||
|
verdict. Optional Boolean facts distinguish negative observations from unknown
|
||||||
|
or inapplicable checks; matching a JSON schema does not prove that a provider
|
||||||
|
enforced it.
|
||||||
|
|
||||||
|
A concluded diagnostic can report failure with a nil Go error. Invalid requests
|
||||||
|
return ordinary errors, and caller cancellation can return partial observations
|
||||||
|
with a recognizable context error. Diagnostics can contact providers and incur
|
||||||
|
inference costs only when explicitly invoked; they are not automatic execution
|
||||||
|
or release checks. See the [smoke diagnostic guide](../consumers/pkg-promptkit.md#run-explicit-smoke-diagnostics)
|
||||||
|
and [API GoDoc](../../smoke_diagnostics.go).
|
||||||
|
|
||||||
|
## Breaking Changes And Upgrade
|
||||||
|
|
||||||
|
Consumers upgrading from v0.15.0 should follow the
|
||||||
|
[configuration and API migration guide](../consumers/configuration-migration.md):
|
||||||
|
|
||||||
|
- Supply backend and profile definitions explicitly. Promptkit no longer bundles
|
||||||
|
provider catalogs or the `promptkit-openrouter-standard` and
|
||||||
|
`promptkit-openrouter-auto` profiles. Conventional backend names do not install
|
||||||
|
definitions automatically. Applications choose and maintain their sources.
|
||||||
|
- Convert version-keyed prompt mappings into one definition per prompt ID.
|
||||||
|
Prompt `version` is now an optional descriptive label, not a selector.
|
||||||
|
Choose deliberately which former definition to install.
|
||||||
|
- Remove `PromptVersion` selectors from `RunRequest`,
|
||||||
|
`RequirementInspectionRequest`, and `SmokeTestRequest`, where applicable.
|
||||||
|
Change prompt inspection calls to `InspectPrompt(ctx, promptID)`. Update code
|
||||||
|
consuming version-bearing inspection, identity, result, or observation fields
|
||||||
|
to handle optional labels, including nil and explicitly empty labels.
|
||||||
|
- When moving resources into collections, update stored backend, profile, and
|
||||||
|
diagnostic-default references to their intended qualified IDs. Mounting a
|
||||||
|
source does not rewrite references; profile inheritance remains within its
|
||||||
|
sibling mapping. Root configuration `version`, when supplied, is now a source
|
||||||
|
specification header, distinct from descriptive resource labels.
|
||||||
|
|
||||||
|
The migration guide includes identity mappings and before/after prompt examples.
|
||||||
|
The [maintained offline example](../../examples/go-library/prepare/main.go)
|
||||||
|
demonstrates explicit composition, source-relative content, scoped preparation,
|
||||||
|
and inventory without contacting a provider.
|
||||||
|
|
||||||
|
After adapting configuration and API calls:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gitea.maximumdirect.net/eric/promptkit@v0.16.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Correctness And Efficiency
|
||||||
|
|
||||||
|
Collection prompt materialization shares one lazy origin-metadata snapshot,
|
||||||
|
avoiding repeated full metadata copies across collections. Internal prepared
|
||||||
|
snapshots detach optional prompt labels. Smoke diagnostics preserve caller
|
||||||
|
cancellation and report actual inference destinations and known-zero usage when
|
||||||
|
no generation request was dispatched.
|
||||||
|
|
||||||
|
[COPYRIGHT](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.16.0/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.16.0/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.16.0/docs/oss/attribution.md)
|
||||||
68
docs/releases/v0.17.0.md
Normal file
68
docs/releases/v0.17.0.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Promptkit v0.17.0
|
||||||
|
|
||||||
|
This release adopts the vanity module path `go.maximumdirect.net/promptkit`
|
||||||
|
and ConfigKit v1.0.0, standardizes configuration resources as identified lists,
|
||||||
|
and removes descriptive prompt-version metadata. These are intentional breaking
|
||||||
|
changes before v1. The minimum supported Go version increases from 1.26.0 to
|
||||||
|
1.27.0 for the ConfigKit v1 migration.
|
||||||
|
|
||||||
|
## Breaking Changes And Upgrade
|
||||||
|
|
||||||
|
Consumers upgrading from v0.16.0 should follow the
|
||||||
|
[configuration and API migration guide](../consumers/configuration-migration.md):
|
||||||
|
|
||||||
|
- Change Promptkit imports from `gitea.maximumdirect.net/eric/promptkit` to
|
||||||
|
`go.maximumdirect.net/promptkit`. Update packages that exchange Promptkit
|
||||||
|
types together; old-path and vanity-path types are distinct.
|
||||||
|
- Change ConfigKit imports to `go.maximumdirect.net/configkit` at v1.0.0 and
|
||||||
|
adopt its renamed public inspection types and fields where used.
|
||||||
|
- Convert root and collection `prompts`, `profiles`, and `backends` from keyed
|
||||||
|
mappings to lists whose records contain an explicit `id`. Collections remain
|
||||||
|
keyed by namespace. Sibling inheritance now uses `extends: {id: parent}`;
|
||||||
|
later same-ID records replace lower records in full.
|
||||||
|
- Change `ProfileSource` and `BackendSource` calls to pass slices of
|
||||||
|
`ProfileConfig` and `BackendConfig`, setting each record's `ID`.
|
||||||
|
- Change source-root `configkit:` controls to `config:`. Imports are optional
|
||||||
|
unless marked `required: true`. Optional Promptkit specification headers now
|
||||||
|
use `version: 2` or `version: "2"`; headerless sources remain valid.
|
||||||
|
- Remove descriptive prompt `version` values and code consuming `PromptVersion`,
|
||||||
|
`PromptIdentity.Version`, or corresponding `prompt_version` output members.
|
||||||
|
Collection `release_version` labels remain available. Prompt hashes may change
|
||||||
|
across this migration.
|
||||||
|
|
||||||
|
After adapting Go imports, configuration, and API calls, use Go 1.27.0 or newer:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get go.maximumdirect.net/promptkit@v0.17.0
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
Earlier tags retain their original module declarations. No permanent local
|
||||||
|
replacement is needed for this cutover. See the
|
||||||
|
[framework format reference](../formats.md) for current configuration semantics
|
||||||
|
and the [maintained preparation example](../../examples/go-library/prepare/main.go)
|
||||||
|
for explicit composition, source-relative imports, and scoped discovery.
|
||||||
|
|
||||||
|
## Configuration Inspection And Execution
|
||||||
|
|
||||||
|
Configuration inspection and diffs expose ConfigKit's composed values and
|
||||||
|
provenance, including list-index paths. Promptkit applies domain normalization
|
||||||
|
and execution defaults to owned interpretations without replacing the source
|
||||||
|
origins needed for relative resources. Profile inspection and preparation expose
|
||||||
|
their interpreted or effective settings. See the
|
||||||
|
[migration guide](../consumers/configuration-migration.md#select-and-compose-the-configuration-you-need)
|
||||||
|
for the distinction and inspection-path updates.
|
||||||
|
|
||||||
|
## Maintainer Tooling
|
||||||
|
|
||||||
|
Release tooling now uses the module-selected ReleaseKit v0.8.0 through `go tool`,
|
||||||
|
with page-only Gitea delivery and saved-publication-attempt recovery. The
|
||||||
|
[release procedure](../release.md) describes publication and recovery.
|
||||||
|
|
||||||
|
The three maintained offline examples now have ordinary Go workflow tests,
|
||||||
|
included in the standard suite. Their separate Python runner and custom release
|
||||||
|
hook have been removed; Python is no longer a normal release prerequisite.
|
||||||
|
The optional release-wiring diagnostic remains Python. See the
|
||||||
|
[development guide](../development.md#maintainer-validation).
|
||||||
|
|
||||||
|
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/v0.17.0/COPYRIGHT) | [LICENSE](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/v0.17.0/LICENSE) | [ATTRIBUTION](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/v0.17.0/docs/oss/attribution.md)
|
||||||
65
docs/releases/v0.18.0.md
Normal file
65
docs/releases/v0.18.0.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Promptkit v0.18.0
|
||||||
|
|
||||||
|
Configuration inspection now exposes Promptkit's recorded field normalization
|
||||||
|
and derived queue-capacity defaults through ConfigKit v1.1.0. Engines also
|
||||||
|
include a default-on OpenRouter configuration. The minimum supported Go
|
||||||
|
version remains 1.27.0.
|
||||||
|
|
||||||
|
## Bundled OpenRouter Backend And Profiles
|
||||||
|
|
||||||
|
New engines include an embedded OpenRouter backend and seventeen root profiles:
|
||||||
|
ten latest-family aliases, the auto and free routers, and five auto-router
|
||||||
|
cost-tier variants. Select a profile explicitly in a prompt or request; the
|
||||||
|
bundle does not supply an implicit execution profile or contact providers
|
||||||
|
during construction. The backend uses `OPENROUTER_API_KEY` when available under
|
||||||
|
the existing optional-credential rules.
|
||||||
|
|
||||||
|
The bundle is the lowest-priority underlay. Applications can replace its records
|
||||||
|
or derive named variants through ordinary configuration, and external collections
|
||||||
|
remain supported. Cost-tier variants inherit the effective `auto` profile, so
|
||||||
|
replacing that parent changes their inherited target while retaining plugins.
|
||||||
|
The [bundle reference](../formats.md#bundled-openrouter-configuration) owns the
|
||||||
|
inventory and composition details.
|
||||||
|
|
||||||
|
Explicit smoke diagnostics now have OpenRouter authentication metadata and an
|
||||||
|
inference fallback profile. They remain opt-in and may incur provider costs.
|
||||||
|
Latest aliases and routers can change upstream; configured slugs do not pin
|
||||||
|
the concrete model, pricing, or availability. See the
|
||||||
|
[consumer guide](../consumers/pkg-promptkit.md#construct-an-engine) for selection
|
||||||
|
and opt-out examples.
|
||||||
|
|
||||||
|
## Recorded Configuration Normalization
|
||||||
|
|
||||||
|
Inspection shows normalized backend and profile settings, prompt metadata, and
|
||||||
|
configured message roles and cache controls, with field-level resolution
|
||||||
|
history. Existing source evidence remains available, and unchanged content-file
|
||||||
|
origins continue to select their resource context.
|
||||||
|
|
||||||
|
Configuration diffs compare normalized effective values, so spelling-only
|
||||||
|
changes that normalize identically may no longer appear. Lists remain atomic in
|
||||||
|
diffs. Inspection does not include backend-to-profile completion or request
|
||||||
|
overrides; see the [configuration inspection guide](../consumers/pkg-promptkit.md#inspect-the-captured-configuration).
|
||||||
|
|
||||||
|
## Compatibility And Upgrade
|
||||||
|
|
||||||
|
New engines install a bundled OpenRouter backend and root profiles by default.
|
||||||
|
Inventory listings and inspection sources therefore change, and references to
|
||||||
|
bundled IDs that previously failed may now resolve. Pass
|
||||||
|
`promptkit.WithBundledOpenRouter(false)` to retain application-only behavior.
|
||||||
|
Caller records can replace bundled definitions through ordinary composition;
|
||||||
|
same-ID replacements replace complete records rather than merging selected fields.
|
||||||
|
No configuration-format migration is required from v0.17.0.
|
||||||
|
Consumers that display or compare configuration inspection results should account
|
||||||
|
for normalized values and derived defaults replacing their earlier authored
|
||||||
|
representations. Request-time overrides and caller-appended messages retain
|
||||||
|
their existing processing.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get go.maximumdirect.net/promptkit@v0.18.0
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [framework format reference](../formats.md) for current configuration
|
||||||
|
semantics. Consumers upgrading from versions before v0.17.0 should also follow
|
||||||
|
the [configuration and API migration guide](../consumers/configuration-migration.md).
|
||||||
|
|
||||||
|
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/v0.18.0/COPYRIGHT) | [LICENSE](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/v0.18.0/LICENSE) | [ATTRIBUTION](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/v0.18.0/docs/oss/attribution.md)
|
||||||
@@ -63,8 +63,8 @@ Every engine now includes the reserved `rakestrawhome` backend, identified by
|
|||||||
selects that backend. Consumers can use the maintained endpoint, credential,
|
selects that backend. Consumers can use the maintained endpoint, credential,
|
||||||
capacity, and model defaults without registering either definition themselves.
|
capacity, and model defaults without registering either definition themselves.
|
||||||
|
|
||||||
See the [built-in backend and profile catalogs](../formats.md#built-in-backends)
|
See the [backend format reference](../formats.md#backend-definitions)
|
||||||
and the [consumer adoption example](../consumers/pkg-promptkit.md#use-the-rakestrawhome-built-in-profile)
|
and the [current identity migration mapping](../consumers/configuration-migration.md#identity-mapping)
|
||||||
for the current contracts.
|
for the current contracts.
|
||||||
|
|
||||||
## Structured Generation Errors
|
## Structured Generation Errors
|
||||||
@@ -126,8 +126,8 @@ cycle-safe, and resolved afresh for ordinary operations. Prepared execution
|
|||||||
freezes the fully resolved target. The selected leaf ID remains public while
|
freezes the fully resolved target. The selected leaf ID remains public while
|
||||||
effective execution settings reflect the resolved chain.
|
effective execution settings reflect the resolved chain.
|
||||||
|
|
||||||
See the [profile inheritance format reference](../formats.md#profile-inheritance),
|
See the [profile inheritance format reference](../formats.md#resource-inheritance),
|
||||||
the [consumer alias example](../consumers/pkg-promptkit.md#alias-a-built-in-profile),
|
the [current identity migration mapping](../consumers/configuration-migration.md#identity-mapping),
|
||||||
and the [`Profile` GoDoc](../../types.go) for exact merge and validation
|
and the [`Profile` GoDoc](../../types.go) for exact merge and validation
|
||||||
behavior.
|
behavior.
|
||||||
|
|
||||||
|
|||||||
@@ -96,9 +96,8 @@ precedence rules. Promptkit no longer keeps duplicate embedded profile assets
|
|||||||
or hard-coded definitions for those maintained backends.
|
or hard-coded definitions for those maintained backends.
|
||||||
|
|
||||||
The module versions in Promptkit's `go.mod` identify the catalog releases
|
The module versions in Promptkit's `go.mod` identify the catalog releases
|
||||||
tested with this release. The [built-in backend and profile format
|
tested with this release. The [backend format reference](../formats.md#backend-definitions)
|
||||||
reference](../formats.md#built-in-backends) remains the canonical consumer
|
remains the canonical consumer contract, while the [internal source documentation](../internal/sources.md#profiles)
|
||||||
contract, while the [internal source documentation](../internal/sources.md#profiles-and-built-ins)
|
|
||||||
describes the dependency boundary.
|
describes the dependency boundary.
|
||||||
|
|
||||||
## Public API Changes
|
## Public API Changes
|
||||||
|
|||||||
131
docs/roadmap/audit/01-correctness.md
Normal file
131
docs/roadmap/audit/01-correctness.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# Functional Correctness Audit
|
||||||
|
|
||||||
|
Reviewed 2026-11-04 against commit `d9f6aad`.
|
||||||
|
|
||||||
|
## Scope And Evidence
|
||||||
|
|
||||||
|
Reviewed the public facade and conversions; ConfigKit handoff, normalization,
|
||||||
|
presence-aware target assembly, collections and namespace selection; artifact
|
||||||
|
loading and template rendering/requirement discovery; ordinary and single-use
|
||||||
|
prepared execution; output validation and repair; backend admission and active
|
||||||
|
permits; HTTP request/response handling and transient retries; execution
|
||||||
|
observations; and smoke-test selection and reporting. Contracts were checked
|
||||||
|
against GoDoc, the format and integration references, internal documentation,
|
||||||
|
and relevant existing tests. Source loading before the ConfigKit handoff remains
|
||||||
|
ConfigKit's responsibility.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- `go test -race -vet=off ./...` passed, including the maintained examples.
|
||||||
|
- `go vet ./...` passed.
|
||||||
|
- An offline, temporary public-API reproduction outside the repository confirmed
|
||||||
|
the number conversion and captured schema payload described below. It used
|
||||||
|
`ProfileSource`, `Prepare`, a runtime override, and `WithSchemaFS` with
|
||||||
|
`fstest.MapFS`; no provider was contacted.
|
||||||
|
|
||||||
|
No production code or tests were changed. This audit does not establish provider
|
||||||
|
availability or acceptance of arbitrary JSON Schema features.
|
||||||
|
|
||||||
|
## 1. Provider Source Helpers Silently Change Exact JSON Numbers
|
||||||
|
|
||||||
|
**Priority:** P2. Confirmed data-conversion defect.
|
||||||
|
|
||||||
|
**Code:** [providerValueForMapping](../../../provider_values.go:51), used by
|
||||||
|
[ProfileSource](../../../profiles.go:33) and
|
||||||
|
[BackendSource](../../../backends.go:100).
|
||||||
|
|
||||||
|
**Circumstances:** A caller supplies a valid `json.Number` in provider
|
||||||
|
`ExtraParams` that cannot be represented exactly by the helper's chosen native
|
||||||
|
number type. After signed and unsigned integer parsing fail, the helper converts
|
||||||
|
the value to `float64` without checking whether that conversion loses precision.
|
||||||
|
The same problem affects sufficiently precise fractional values.
|
||||||
|
|
||||||
|
**Expected:** A source helper preserves the meaning of a supported provider
|
||||||
|
value, or reports that the value cannot be represented. It must not silently
|
||||||
|
substitute a different number. This concerns PromptKit's own convenience-source
|
||||||
|
conversion, not additional validation of ConfigKit's raw sources.
|
||||||
|
|
||||||
|
**Actual / reproduction:** With
|
||||||
|
`ExtraParams: map[string]any{"number": json.Number("18446744073709551617")}`
|
||||||
|
on a profile, `NewEngine` succeeds. JSON encoding of
|
||||||
|
`Prepare(...).EffectiveModelParams.ExtraParams` produces:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"number":18446744073709552000}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supplying the identical value through `RunRequest.Execution.ExtraParams`
|
||||||
|
instead produces:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"number":18446744073709551617}
|
||||||
|
```
|
||||||
|
|
||||||
|
The built-in client serializes those effective values directly into the request,
|
||||||
|
so configuration-source selection changes the actual provider parameter.
|
||||||
|
`BackendSource` shares the same converter. No high-precision numeric preservation
|
||||||
|
claim needs to be added for arbitrary source formats to correct this discrepancy.
|
||||||
|
|
||||||
|
**Potential remediation:** Preserve exact numbers through a numeric
|
||||||
|
representation supported by ConfigKit, if available. If that contract cannot
|
||||||
|
represent a particular `json.Number` exactly, surface a deferred source error
|
||||||
|
rather than returning a rounded float or a numeric-looking string. Check numeric
|
||||||
|
value equivalence rather than textual spelling: alternate exponent spellings
|
||||||
|
need not be preserved. Add focused regression coverage for both source helpers
|
||||||
|
and contrast with the already-preserving runtime path.
|
||||||
|
|
||||||
|
## 2. External Schema Resources Do Not Reach The Provider Constraint
|
||||||
|
|
||||||
|
**Priority:** P2. Confirmed schema-packaging gap; remediation requires choosing
|
||||||
|
how provider-facing schemas accommodate the existing local-reference feature.
|
||||||
|
|
||||||
|
**Code:** [preparedValidation.SchemaDocument](../../../internal/validate/standard_validator.go:63),
|
||||||
|
[StandardValidator.PrepareValidation](../../../internal/validate/standard_validator.go:74),
|
||||||
|
[FSValidator.PrepareValidation](../../../internal/validate/standard_validator.go:126),
|
||||||
|
and [structuredOutputFromValidationPlan](../../../internal/usecase/runner.go:419).
|
||||||
|
|
||||||
|
**Circumstances:** A JSON Schema output contract uses a root schema referring to
|
||||||
|
another file within the configured schema source. The
|
||||||
|
[schema format contract](../../formats.md#schemas) supports such references.
|
||||||
|
Preparation compiles the transitive resources locally, but `SchemaDocument`
|
||||||
|
exposes only the unchanged root document. The runner always uses that document
|
||||||
|
for the native provider constraint in JSON Schema mode.
|
||||||
|
|
||||||
|
**Expected:** The provider-facing constraint should carry the schema resources
|
||||||
|
needed to interpret the supported local references, or there should be an
|
||||||
|
explicitly chosen way to retain local validation without submitting an
|
||||||
|
unresolvable native constraint. This is not a promise that every provider accepts
|
||||||
|
every schema dialect feature.
|
||||||
|
|
||||||
|
**Actual / reproduction:** With these files under `WithSchemaFS`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
root.json: {"type":"object","properties":{"x":{"$ref":"child.json"}},"required":["x"]}
|
||||||
|
child.json: {"type":"integer"}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Prepare` succeeds, but `PreparedRun.StructuredOutput.JSONSchema.Schema` contains
|
||||||
|
only the root object, including the literal `$ref: "child.json"`. No child
|
||||||
|
document, bundled definition, or reference rewrite is present. The HTTP adapter
|
||||||
|
passes that object unchanged to `response_format.json_schema.schema`. An upstream
|
||||||
|
provider has no access to the application filesystem or private `fs.FS`, so it
|
||||||
|
cannot resolve that reference from the request. A provider requiring resolvable
|
||||||
|
native schemas can reject generation before local validation or repair is
|
||||||
|
reached. No live-provider rejection was tested.
|
||||||
|
|
||||||
|
The internal source document accurately describes the existing root-only
|
||||||
|
projection; the gap is between the supported local schema-resource graph and
|
||||||
|
its mandatory projection into an outbound constraint, rather than a failure of
|
||||||
|
local compilation or prepared-resource freezing. Self-contained schemas are
|
||||||
|
unaffected.
|
||||||
|
|
||||||
|
**Potential remediation:** Produce a self-contained provider schema from the
|
||||||
|
operation's captured resources, preserving reference semantics, identifiers,
|
||||||
|
anchors, and recursion. Avoid naive recursive textual substitution. An
|
||||||
|
alternative is an explicit choice to use local-only schema validation when
|
||||||
|
native submission cannot represent the resource graph. Any alternative that
|
||||||
|
restricts existing schema sources or silently disables native structured output
|
||||||
|
requires a deliberate compatibility/policy decision. Protect the selected
|
||||||
|
behavior with an offline test combining transitive schema preparation and the
|
||||||
|
actual outgoing request, while retaining frozen local validation for
|
||||||
|
`RunPrepared` and repair.
|
||||||
147
docs/roadmap/audit/02-efficiency.md
Normal file
147
docs/roadmap/audit/02-efficiency.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# Algorithmic Efficiency Audit
|
||||||
|
|
||||||
|
Reviewed 2026-11-04 against commit `0d27252`.
|
||||||
|
|
||||||
|
## Scope And Method
|
||||||
|
|
||||||
|
Reviewed PromptKit-owned configuration assembly, provider-value conversion and
|
||||||
|
copying, rendering and requirement analysis, inventory selection, prepared-run
|
||||||
|
ownership, validation orchestration, repair formatting, retry coordination,
|
||||||
|
observations, and capacity management. Findings below follow from implementation
|
||||||
|
and operation counts; no benchmarks or live-provider calls were needed.
|
||||||
|
|
||||||
|
The public facade delegates to internal components. ConfigKit supplies the final
|
||||||
|
configuration; PromptKit constructs immutable runtime indexes and captured
|
||||||
|
templates. Execution uses operation-local validation plans, shared backend
|
||||||
|
capacity, and a generation coordinator. Inspection reads the captured snapshot.
|
||||||
|
ConfigKit and ReleaseKit internals are outside this repository audit.
|
||||||
|
|
||||||
|
### Deliberate Choices Preserved
|
||||||
|
|
||||||
|
Checked current internal documentation and relevant retired plans in Git history,
|
||||||
|
including the performance plan at `8a5c0de`, configuration adoption at `5f7b194`,
|
||||||
|
and collections at `89af2a1`. This audit does not reopen:
|
||||||
|
|
||||||
|
- lexical inventory ordering, recursive provider-map overlay semantics, or
|
||||||
|
conservative requirement-discovery semantics;
|
||||||
|
- captured parsing, per-render artifact-text reuse, operation-local schema
|
||||||
|
compilation, frozen retry payloads, and linked-list FIFO permits with
|
||||||
|
constant-time cancellation removal;
|
||||||
|
- the existing JSON depth/work bounds, output/diagnostic limits, or copies
|
||||||
|
deliberately retained to protect public and injected-collaborator ownership;
|
||||||
|
- synchronous filesystem/template/schema operations or the selected retry and
|
||||||
|
repair budgets.
|
||||||
|
|
||||||
|
The opportunities below concern implementation work that those choices do not
|
||||||
|
require. Complexity statements use expected constant-time Go map operations and
|
||||||
|
treat identifier comparison/hashing cost separately. No new public performance
|
||||||
|
guarantee, cache, caller limit, or release gate is proposed.
|
||||||
|
|
||||||
|
## 1. Condition Evidence Unions Can Require Quadratic Work
|
||||||
|
|
||||||
|
**Priority:** P2. Requirement-inspection path; most relevant to deeply nested
|
||||||
|
conditions rather than ordinary small templates.
|
||||||
|
|
||||||
|
**Code:** [evaluateCommand](../../../internal/prompt/requirements.go:142) and
|
||||||
|
[mergeConditionEvaluations](../../../internal/prompt/requirements.go:318).
|
||||||
|
|
||||||
|
Each comparison recursively evaluates its operands, then allocates fresh
|
||||||
|
`missingVariables` and `contentInputs` maps and copies both operands' accumulated
|
||||||
|
evidence. A left-heavy expression with distinct missing caller keys, such as
|
||||||
|
successively nesting `eq (eq .v0 .v1) .v2`, carries an increasingly large missing
|
||||||
|
key set through each ancestor. Template parsing accepts these expressions;
|
||||||
|
inspection can encounter them even when their eventual execution would fail or
|
||||||
|
the inspection result is partial.
|
||||||
|
|
||||||
|
For an expression of size n, successive unions copy approximately
|
||||||
|
2 + 3 + ... + n entries: **Theta(n²) total set work and cumulative allocation**.
|
||||||
|
The final unique evidence has only O(n) entries. This is separate from the
|
||||||
|
previously fixed repeated syntax-subtree scan: visiting each syntax node once
|
||||||
|
does not eliminate repeated copies of the evidence produced by those nodes.
|
||||||
|
|
||||||
|
**Opportunity:** Transfer ownership of temporary sets and merge the smaller set
|
||||||
|
into the larger one, rather than copying both at every comparison. For this
|
||||||
|
expression family that is O(n); small-to-large merging gives O(n log n) aggregate
|
||||||
|
set-insertion work across general expression trees. Another option is an
|
||||||
|
operation-local evidence accumulator that records each discovery directly,
|
||||||
|
while keeping only condition truth/limitation state on recursive return values.
|
||||||
|
Choose the simpler approach after checking temporary-set ownership. Preserve
|
||||||
|
current grammar, classifications, deduplication, sorted output, cancellation,
|
||||||
|
and partial-result behavior; interpreting more expressions is not necessary.
|
||||||
|
|
||||||
|
## 2. Recursive Provider Overlay Repeatedly Clones Descendant Subtrees
|
||||||
|
|
||||||
|
**Priority:** P3. Construction-time improvement with an existing depth bound.
|
||||||
|
|
||||||
|
**Code:** [overlayProviderMap](../../../internal/configuration/targets.go:131).
|
||||||
|
|
||||||
|
The helper deep-copies the entire lower map before considering overrides. When
|
||||||
|
both values at a key are maps, it recursively invokes itself on a descendant
|
||||||
|
that was already copied, and copies that whole descendant again. The first copy
|
||||||
|
is then replaced and discarded. All descendants on an overlapping path can be
|
||||||
|
copied once per ancestor.
|
||||||
|
|
||||||
|
Let N be the lower tree's node count and D its overlapping map depth. Node-copy
|
||||||
|
work can reach **O(ND)**. For a narrow chain it is the triangular sum
|
||||||
|
D + (D-1) + ... + 1. A wide K-entry leaf below D overlapping ancestors is cloned
|
||||||
|
roughly D times instead of once. `jsonvalue.CopyMap` also sorts map keys, so the
|
||||||
|
same wide leaf's sorting work is repeated at those ancestors.
|
||||||
|
|
||||||
|
The existing maximum container depth of 100 is important: this is **not an
|
||||||
|
unbounded quadratic-in-N claim under today's fixed limits**. It is an avoidable
|
||||||
|
depth-dependent multiplier in traversal and cumulative allocations within those
|
||||||
|
limits, multiplied again when multiple profiles overlay the same backend. That
|
||||||
|
bound and the recursive-map/value-replacement semantics remain selected choices.
|
||||||
|
|
||||||
|
**Opportunity:** Clone the lower tree once, then overlay into the owned clone
|
||||||
|
without re-cloning existing descendant maps. Copy incoming replacement values
|
||||||
|
where needed to preserve isolation from configuration inputs. Alternatively,
|
||||||
|
construct the merged output in one traversal. Either removes the repeated
|
||||||
|
ancestor copies: expected O(N + M) merge traversal for lower/upper trees of sizes
|
||||||
|
N and M, apart from the retained validation/sorting costs. Preserve null and
|
||||||
|
array replacement, empty-map behavior, input ownership, and existing JSON
|
||||||
|
validation/bounds; do not replace ConfigKit's configuration machinery.
|
||||||
|
|
||||||
|
## 3. Filtered Listings Copy And Scan The Complete Inventory
|
||||||
|
|
||||||
|
**Priority:** P3. Scalability opportunity for engines with many namespaces.
|
||||||
|
|
||||||
|
**Code:** [filterIdentityList](../../../internal/engine/engine.go:306),
|
||||||
|
[ListPrompts](../../../internal/engine/engine.go:270), and the
|
||||||
|
[configuration inventory methods](../../../internal/configuration/listing.go:14).
|
||||||
|
|
||||||
|
A named-namespace request first obtains a detached copy of all N identities and
|
||||||
|
then scans it to build its k matching results. Even an absent namespace costs
|
||||||
|
**Theta(N) time and O(N) temporary storage**, rather than work proportional to
|
||||||
|
the selected inventory. Listing C namespaces separately costs Theta(CN); when
|
||||||
|
each has a bounded number of entries, that becomes quadratic in total inventory
|
||||||
|
size despite only O(N) total returned entries.
|
||||||
|
|
||||||
|
The collection implementation plan at `89af2a1` expressly allowed namespace
|
||||||
|
buckets or sorted ranges to avoid repeated full scans; it did not require the
|
||||||
|
present whole-inventory-copy algorithm. The lexical ordering and caller-owned
|
||||||
|
result contracts do not prevent indexed selection.
|
||||||
|
|
||||||
|
**Opportunity:** Capture per-kind namespace buckets or ranges alongside existing
|
||||||
|
construction-time inventories, then copy only the selected entries. Buckets
|
||||||
|
allow expected O(1 + k) selection/copy; binary search over sorted full IDs can
|
||||||
|
support named-prefix ranges in O(log N + k). Keep unqualified and all-ID modes
|
||||||
|
correct without assuming unqualified entries form one contiguous lexical range.
|
||||||
|
Resolve filters before requesting the inventory, and retain exact namespace
|
||||||
|
matching, full IDs, ordering, context behavior, and independent returned slices.
|
||||||
|
An all-ID request must still spend O(N) to return N owned entries; that is not a
|
||||||
|
defect. No public pagination API or mutable cache is needed.
|
||||||
|
|
||||||
|
## Review Outcome
|
||||||
|
|
||||||
|
These three opportunities are supported by direct operation-count analysis;
|
||||||
|
they are not measured latency claims. The largest asymptotic issue is growing
|
||||||
|
condition-evidence copying. The overlay finding is explicitly bounded, and the
|
||||||
|
listing finding matters as independent namespace count grows.
|
||||||
|
|
||||||
|
No additional ordinary correctness defect was established in this pass. The
|
||||||
|
findings in [the correctness audit](01-correctness.md) remain separate and were
|
||||||
|
not duplicated here. No production code, tests, dependencies, or established
|
||||||
|
behavior were changed, so the successful race-suite/vet evidence recorded there
|
||||||
|
still applies to the unchanged implementation; those checks were not rerun just
|
||||||
|
for this documentation change.
|
||||||
216
docs/roadmap/audit/03-complexity.md
Normal file
216
docs/roadmap/audit/03-complexity.md
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
# Complexity And Maintainability Audit
|
||||||
|
|
||||||
|
Reviewed 2026-11-04 against commit `5adc1c1`.
|
||||||
|
|
||||||
|
## Method And Architectural Assessment
|
||||||
|
|
||||||
|
Used the local codebase-memory graph's cyclomatic/cognitive complexity,
|
||||||
|
function-length, and call-relationship tools to locate candidates. Reviewed
|
||||||
|
functions and methods, including test hotspots, rather than relying on
|
||||||
|
function-only rankings. Inspected implementation, callers, owning interfaces,
|
||||||
|
and current architecture/internal documentation before selecting findings.
|
||||||
|
Metrics are analyzer-specific indicators, not correctness proofs, acceptance
|
||||||
|
thresholds, or requirements to split every large switch.
|
||||||
|
|
||||||
|
PromptKit's overall boundaries remain useful: a public facade adapts values and
|
||||||
|
errors; configuration interprets ConfigKit's final handoff and captures runtime
|
||||||
|
indexes; the runner coordinates preparation and execution; generation owns
|
||||||
|
attempts/retries; capacity owns admission and permits; transport and schema
|
||||||
|
adapters own external protocols. No repository-wide architectural rewrite or new
|
||||||
|
public abstraction is warranted by this review.
|
||||||
|
|
||||||
|
Representative hotspot metrics from the current graph:
|
||||||
|
|
||||||
|
| Function | Cyclomatic | Cognitive |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| `requirementWalker.visit` | 50 | 137 |
|
||||||
|
| `validateEffective` | 39 | 132 |
|
||||||
|
| `requirementWalker.evaluateCommand` | 26 | 103 |
|
||||||
|
| `providerValueForMapping` | 33 | 84 |
|
||||||
|
| `jsonvalue.copyValue` | 29 | 66 |
|
||||||
|
| `loaded.buildExecutionProfiles` | 27 | 58 |
|
||||||
|
| `loaded.materializePrompt` | 30 | 52 |
|
||||||
|
| `Coordinator.generate` | 24 | 49 |
|
||||||
|
|
||||||
|
The findings below are behavior-preserving refactoring opportunities, not
|
||||||
|
confirmed bugs or automatic release blockers. They cover both package-local
|
||||||
|
complexity and interactions between packages. No production code, tests, or
|
||||||
|
dependencies were changed, and no empirical testing or benchmarks were needed.
|
||||||
|
|
||||||
|
## 1. Consolidate Schema Compilation And Express The Runner's Actual Interface
|
||||||
|
|
||||||
|
**Code:** [validator interfaces](../../../internal/validate/validator.go:10),
|
||||||
|
[schema implementations](../../../internal/validate/standard_validator.go:45),
|
||||||
|
[runner preparation](../../../internal/usecase/runner.go:398), and
|
||||||
|
[engine assembly](../../../internal/engine/engine.go:133).
|
||||||
|
|
||||||
|
**Complexity:** The validator maintains direct validation and prepared
|
||||||
|
validation, with separate filesystem and `fs.FS` compilation paths. Compiler
|
||||||
|
construction, resource registration, error wrapping, and cancellation checks
|
||||||
|
are repeated across direct schema-validation methods and `PrepareValidation`.
|
||||||
|
The package's constructors return `Validator`, although the runner actually
|
||||||
|
requires `ValidationPreparer` and discovers it through a runtime type assertion.
|
||||||
|
An implementation satisfying the declared constructor dependency can therefore
|
||||||
|
still be unsuitable for the execution path.
|
||||||
|
|
||||||
|
**Opportunity:** Make the runner's preparation dependency explicit in its
|
||||||
|
internal field/constructor types, updating assembly and focused test
|
||||||
|
collaborators together. Retain the direct validation interface where callers
|
||||||
|
actually need it. Consolidate compilation behind one internal operation, with
|
||||||
|
small source-specific loading/URL/loader adapters; direct and prepared callers
|
||||||
|
can reuse that operation rather than separately implementing registration and
|
||||||
|
compilation. No public validator API or generic source framework is needed.
|
||||||
|
|
||||||
|
**Preserve:** Direct validation currently rejects malformed JSON before trying
|
||||||
|
to compile its schema; simply implementing direct `Validate` as eager
|
||||||
|
`PrepareValidation` followed by plan validation would change that error/I/O
|
||||||
|
ordering. Keep that short-circuit, operation-local compiled resources, prepared
|
||||||
|
freezing, schema dialect and containment rules, synchronous cancellation
|
||||||
|
precedence, and the runner's existing nil-validator behavior where used.
|
||||||
|
Coordinate schema representation changes with correctness-audit finding 2,
|
||||||
|
without treating this refactor alone as its fix.
|
||||||
|
|
||||||
|
## 2. Decompose Configuration Validation And Resource Materialization
|
||||||
|
|
||||||
|
**Code:** [validateEffective](../../../internal/configuration/load.go:410),
|
||||||
|
[materializePrompt](../../../internal/configuration/prompts.go:58),
|
||||||
|
[buildExecutionProfiles](../../../internal/configuration/targets.go:14), and
|
||||||
|
[resolution callbacks](../../../internal/configuration/resolve_prompts.go:10).
|
||||||
|
|
||||||
|
**Complexity:** `validateEffective` combines namespace checks, reference indexes,
|
||||||
|
backend settings, capacity arithmetic, smoke metadata, profile bounds, and prompt
|
||||||
|
references in one nested traversal. Prompt materialization then combines input
|
||||||
|
validation, presence projection, content-file I/O, message validation, template
|
||||||
|
compilation, output-contract construction, hashing, and inspection assembly.
|
||||||
|
Target assembly is another long presence-state decision chain. Reviewing a
|
||||||
|
single field change requires following several distinct responsibilities inside
|
||||||
|
large functions.
|
||||||
|
|
||||||
|
**Opportunity:** Keep the top-level traversal/index preparation visible, but
|
||||||
|
delegate coherent record-level work to backend, profile, and prompt helpers
|
||||||
|
with explicit record/path/index arguments. Isolate prompt input projection,
|
||||||
|
message materialization/compilation, and output-contract construction into
|
||||||
|
small functions returning the values the caller needs. Keep target presence
|
||||||
|
interpretation in one identifiable helper rather than distributing it between
|
||||||
|
new callers. Organize files by those responsibilities only where that helps
|
||||||
|
navigation; moving unchanged code between files is not itself the improvement.
|
||||||
|
|
||||||
|
**Preserve:** Validate only final ConfigKit-provided values. Keep recorded
|
||||||
|
normalization, field presence/null distinctions, exact references, error
|
||||||
|
identities, source-relative resource contexts, lazy metadata capture, and
|
||||||
|
construction-time template/hash capture. Materialization legitimately combines
|
||||||
|
validation with resource access; this finding does not require moving every
|
||||||
|
check into a new validation pass or aggregating formerly fail-fast errors.
|
||||||
|
Avoid a reflection-driven field schema that obscures the domain rules.
|
||||||
|
|
||||||
|
## 3. Untangle Requirement Traversal, Expression Evaluation, And Evidence
|
||||||
|
|
||||||
|
**Code:** [requirement walker](../../../internal/prompt/requirements.go:356),
|
||||||
|
[syntax dispatch](../../../internal/prompt/requirements.go:451),
|
||||||
|
[condition evaluation](../../../internal/prompt/requirements.go:93), and
|
||||||
|
[command traversal](../../../internal/prompt/requirements.go:652).
|
||||||
|
|
||||||
|
**Complexity:** The approximately thousand-line file contains mutually recursive
|
||||||
|
syntax walking and partial expression evaluation, branch reachability, caller
|
||||||
|
scope tracking, template-call handling, evidence promotion, issue construction,
|
||||||
|
and result sorting. `if` and `with` repeat branch orchestration. `input`, `index`,
|
||||||
|
root variables, and skipped-path behavior appear in both evaluation and
|
||||||
|
conservative traversal. Correctness depends on the interaction of return-value
|
||||||
|
flags and mutable walker state such as `rootKnown`, `dependencies`, and the
|
||||||
|
unknown-effect flags, not just the local switch case.
|
||||||
|
|
||||||
|
**Opportunity:** Separate the existing roles into small internal units: syntax
|
||||||
|
recognition/evaluation, branch/scope traversal, and evidence/result accumulation.
|
||||||
|
Factor shared `if`/`with` orchestration while making their different body-dot
|
||||||
|
rules explicit. Reuse recognition of supported command shapes instead of
|
||||||
|
duplicating it between evaluative and inventory-only paths. Make existing
|
||||||
|
template scope entry/exit state handling explicit. Keep a small enum/value model
|
||||||
|
rather than adding a general expression interpreter or pluggable AST framework.
|
||||||
|
|
||||||
|
**Preserve:** The deliberately conservative recognition scope, skipped-path
|
||||||
|
inventory, optional `index` behavior, named-template limitations, incomplete
|
||||||
|
results, and cancellation semantics. Traversal and evaluation are not
|
||||||
|
interchangeable: inactive paths must retain recognizable names without
|
||||||
|
evaluating caller-derived names. Any shared helper must express that distinction.
|
||||||
|
The evidence-union efficiency finding in [audit 02](02-efficiency.md) is related
|
||||||
|
but separate; a mechanical file split would not fix it.
|
||||||
|
|
||||||
|
## 4. Clarify Ownership And Policies Across The JSON Value Walkers
|
||||||
|
|
||||||
|
**Code:** [source mapping conversion](../../../provider_values.go:31),
|
||||||
|
[bounded JSON copying](../../../internal/jsonvalue/jsonvalue.go:67), and
|
||||||
|
[public-value copying](../../../convert.go:406).
|
||||||
|
|
||||||
|
**Complexity:** Three recursive reflection-based paths independently handle
|
||||||
|
interfaces/pointers, maps, sequences, scalars, and nil values. Each adds its own
|
||||||
|
policy: source conversion translates into ConfigKit-compatible containers and
|
||||||
|
defers unsupported-value errors; `jsonvalue` validates and bounds owned copies;
|
||||||
|
facade copying detaches already-valid values while preserving their types.
|
||||||
|
Similar-looking switches conceal different assumptions and failure behavior.
|
||||||
|
The numeric conversion defect in correctness-audit finding 1 demonstrates a
|
||||||
|
concrete risk at one of these boundaries, but is not a new finding here.
|
||||||
|
|
||||||
|
**Opportunity:** First make the three responsibilities explicit at their call
|
||||||
|
boundaries. Isolate source-number encoding from structural traversal, and
|
||||||
|
identify reusable scalar/indirection/container-copy primitives only where their
|
||||||
|
semantics truly match. Prefer removing a redundant traversal path or reusing an
|
||||||
|
existing trusted-value copy operation over maintaining another policy-rich
|
||||||
|
reflection switch. Keep ConfigKit-specific mapping encoding in its adapter;
|
||||||
|
do not teach the source-neutral domain about ConfigKit.
|
||||||
|
|
||||||
|
**Preserve:** Supported concrete types, nil behavior, cycle/depth/work handling,
|
||||||
|
caller ownership, and deferred source errors. In particular, passing source
|
||||||
|
builders through rejecting `CopyMap` would introduce validation before the
|
||||||
|
ConfigKit handoff and is not an acceptable consolidation. Nor should an
|
||||||
|
infallible facade projection acquire new rejection behavior merely to reuse a
|
||||||
|
validator. A universal callback-configured reflection framework could increase
|
||||||
|
complexity; retain separate small adapters where their policies differ.
|
||||||
|
|
||||||
|
## 5. Extract Attempt Accounting From Generation Control Flow
|
||||||
|
|
||||||
|
**Code:** [Coordinator.generate](../../../internal/generation/coordinator.go:98),
|
||||||
|
[generateInjected](../../../internal/generation/coordinator.go:203),
|
||||||
|
[execute](../../../internal/generation/coordinator.go:303), and
|
||||||
|
[diagnostic evidence consumers](../../../internal/usecase/smoke_tester.go:246).
|
||||||
|
|
||||||
|
**Complexity:** The coordinator interleaves permit acquisition, dispatch,
|
||||||
|
cancellation, retry decisions, observer events, diagnostic evidence, and
|
||||||
|
presence-aware usage accumulation. The built-in and injected paths repeat
|
||||||
|
generation-number setup, evidence finishing, attempt projection, and usage
|
||||||
|
projection. The injected path also repeats its non-dispatched cancellation
|
||||||
|
reporting. `execute` returns five parallel values, increasing the amount of
|
||||||
|
state the caller must keep aligned while constructing two reporting forms.
|
||||||
|
|
||||||
|
**Opportunity:** Return one small internal attempt-result value containing
|
||||||
|
outcome and timing/dispatch facts, and extract common projection/accounting
|
||||||
|
operations where evidence is genuinely shared. Keep the retry loop primarily
|
||||||
|
about eligibility, budget, waits, and terminal outcomes. Keep injected and
|
||||||
|
built-in dispatch strategies explicit rather than manufacturing HTTP facts for
|
||||||
|
injected clients or introducing an extensible retry framework.
|
||||||
|
|
||||||
|
**Preserve:** Release active permits before callbacks/backoff, one-shot injected
|
||||||
|
dispatch, frozen built-in requests, usage-presence distinctions, private
|
||||||
|
diagnostic evidence, ordinary observer boundaries, and retention of decoded
|
||||||
|
facts before cancellation discards ordinary results. The HTTP adapter should
|
||||||
|
continue owning transport facts; accounting extraction must not push provider
|
||||||
|
protocol knowledge into the runner or observation package.
|
||||||
|
|
||||||
|
## Prioritization And Non-Findings
|
||||||
|
|
||||||
|
Start with the schema interface/compilation consolidation and configuration
|
||||||
|
helpers: their responsibilities provide comparatively clear refactoring seams.
|
||||||
|
The walker and generation coordinator need narrower, behavior-preserving units
|
||||||
|
because their state transitions are more subtle. Coordinate source conversion
|
||||||
|
cleanup with the independently required number-preservation fix.
|
||||||
|
|
||||||
|
High scores alone did not justify findings for explicit public error-category
|
||||||
|
switches, presence-aware source-field encoding, regular-file/schema read loops,
|
||||||
|
or table-driven tests. Those branches often directly express selected contracts.
|
||||||
|
Large facade files also include substantial GoDoc and straightforward value
|
||||||
|
projection, which is not evidence that public/internal separation has failed.
|
||||||
|
Keep useful test scenarios; do not rewrite tests solely to reduce their metrics.
|
||||||
|
|
||||||
|
No additional ordinary correctness defect was established in this pass.
|
||||||
|
[Audit 01](01-correctness.md) and [audit 02](02-efficiency.md) retain their own
|
||||||
|
findings. No implementation changes or additional consumer guarantees are
|
||||||
|
implied by this refactoring catalog.
|
||||||
215
docs/roadmap/audit/05-resources.md
Normal file
215
docs/roadmap/audit/05-resources.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# Resource Management Audit
|
||||||
|
|
||||||
|
Reviewed 2026-11-04 against commit `fb87fab`.
|
||||||
|
|
||||||
|
## Scope And Evidence
|
||||||
|
|
||||||
|
Reviewed file and response-body ownership, bounded reads, HTTP client/transport
|
||||||
|
lifetimes, timeout contexts and retry timers, backend admission and permits,
|
||||||
|
prepared-handle cleanup, retained snapshots, JSON conversion, rendering buffers,
|
||||||
|
schema resources, and repair feedback. The architecture uses immutable
|
||||||
|
engine-lifetime configuration, invocation-local execution/validation resources,
|
||||||
|
and caller-owned public results. Production code launches no PromptKit-owned
|
||||||
|
goroutines or background workers requiring a shutdown protocol.
|
||||||
|
|
||||||
|
Evidence consists of source/call-graph review, relevant existing tests, and a
|
||||||
|
small temporary offline diagnostic outside the repository. The diagnostic used
|
||||||
|
a controlled HTTP transport, counted body reads/closure, inspected the existing
|
||||||
|
prepared-handle owner reference, and compared string backing addresses. No
|
||||||
|
benchmark, heap-size measurement, deliberate exhaustion, live network call,
|
||||||
|
production change, or new repository test was needed.
|
||||||
|
|
||||||
|
Focused existing tests passed for content-file closure, HTTP error-body limits
|
||||||
|
and closure, independent attempt contexts/body closure, read cancellation,
|
||||||
|
prepared discard/ownership, diagnostic formatting, synchronous schema
|
||||||
|
cancellation, and repeated-value JSON work bounds. These tests establish the
|
||||||
|
existing protected behavior; they do not cover all findings below.
|
||||||
|
|
||||||
|
## 1. Authentication Diagnostics Read An Unbounded Upstream Body
|
||||||
|
|
||||||
|
**Priority:** P2. Unbounded remote-response consumption; a finite limit would be
|
||||||
|
a new policy decision, not enforcement of an existing documented byte limit.
|
||||||
|
|
||||||
|
**Code:** [runOpenRouterKey](../../../internal/usecase/smoke_authentication.go:66),
|
||||||
|
[recognizedOpenRouterKeyResponse](../../../internal/usecase/smoke_authentication.go:139),
|
||||||
|
and [diagnostic HTTP execution](../../../internal/llm/http_evidence.go:129).
|
||||||
|
|
||||||
|
**Circumstances:** An authentication endpoint returns HTTP 200 with a large or
|
||||||
|
continuously growing body. Recognition calls unrestricted `io.ReadAll` and then
|
||||||
|
unmarshals the whole document into raw-message maps. Neither declared length nor
|
||||||
|
streamed bytes are bounded on this path. Generation's 16 MiB success-body limit
|
||||||
|
and 64 KiB error-body limit do not apply to diagnostic GETs.
|
||||||
|
|
||||||
|
**Consequences / evidence:** The offline diagnostic supplied a valid small
|
||||||
|
identifying `data` object plus 4 MiB of irrelevant padding. The probe read all
|
||||||
|
4,194,375 bytes, closed the body, and reported credential acceptance. Increasing
|
||||||
|
the padding has no corresponding byte-budget check. A fast misconfigured or
|
||||||
|
unexpected endpoint can consume substantial memory before its timeout; concurrent
|
||||||
|
authentication probes multiply consumption because HTTP-only diagnostics bypass
|
||||||
|
generation admission. A timeout bounds elapsed time, not bytes allocated within
|
||||||
|
that time. There is no demonstrated descriptor leak: the body is closed.
|
||||||
|
|
||||||
|
**Potential remediation:** Select a finite body budget appropriate to this small
|
||||||
|
diagnostic protocol, enforce it for both declared and streamed/underreported
|
||||||
|
lengths, and close without unbounded draining on overflow. Preserve single-JSON
|
||||||
|
framing and the distinction between unknown acceptance and observed rejection;
|
||||||
|
an oversized response must not be reported as credential rejection. Decide the
|
||||||
|
budget and overflow report classification explicitly, then update the owning
|
||||||
|
contract and tests. This need not impose new limits on application prompts,
|
||||||
|
artifacts, or generated output.
|
||||||
|
|
||||||
|
## 2. Convenience Source Conversion Expands Shared Graphs Before Bounded Loading
|
||||||
|
|
||||||
|
**Priority:** P2. Potentially exponential allocation before upstream resource
|
||||||
|
checks can run.
|
||||||
|
|
||||||
|
**Code:** [providerMapForMapping](../../../provider_values.go:19),
|
||||||
|
[providerValueForMapping](../../../provider_values.go:31),
|
||||||
|
[ProfileSource](../../../profiles.go:33), and
|
||||||
|
[BackendSource](../../../backends.go:100).
|
||||||
|
|
||||||
|
**Circumstances:** A caller builds a small acyclic Go value graph containing
|
||||||
|
repeated map/slice references. Each level can point twice to the same child:
|
||||||
|
|
||||||
|
```go
|
||||||
|
value := map[string]any{"value": true}
|
||||||
|
for range depth {
|
||||||
|
value = map[string]any{"left": value, "right": value}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The source converter tracks only active ancestors to identify cycles. Once one
|
||||||
|
branch finishes, its visit is removed, so the sibling independently converts
|
||||||
|
the same entire child. It allocates a fresh expanded tree before calling
|
||||||
|
`configkit.Mapping`. For d levels, O(d) distinct input maps become
|
||||||
|
Theta(2^d) output maps. The depth guard does not constrain width or total work;
|
||||||
|
this expansion can become impractical well below depth 100.
|
||||||
|
|
||||||
|
**Consequences:** Programmatic source creation can exhaust memory before
|
||||||
|
`NewEngine` or ConfigKit's deferred diagnostics are reached. This is not a claim
|
||||||
|
that ConfigKit lacks bounds: the selected ConfigKit v1.1.0's mapping builder
|
||||||
|
checks node, byte, and depth budgets during its own snapshot traversal
|
||||||
|
(`internal/engine/value.go`, `valueBuilder.build`). It is called only after
|
||||||
|
PromptKit has already expanded the graph. PromptKit's later `jsonvalue` node
|
||||||
|
budget is also too late to protect this conversion.
|
||||||
|
|
||||||
|
**Potential remediation:** Memoize completed structural conversions so repeated
|
||||||
|
input containers reuse already-converted containers until ConfigKit snapshots
|
||||||
|
them with its existing bounds. Keep active-ancestor tracking distinct from the
|
||||||
|
completed memo so cycles still become deferred source errors. Account correctly
|
||||||
|
for container identity, including distinct slice views. Alternatively, seek
|
||||||
|
upstream support for the additional Go value forms so this conversion can be
|
||||||
|
removed. Preserve immediate source ownership and deferred failure behavior;
|
||||||
|
do not introduce a PromptKit raw-source validation pass or duplicate ConfigKit's
|
||||||
|
configuration limits to fix this pre-handoff allocation problem.
|
||||||
|
|
||||||
|
## 3. Inert Prepared Handles Retain Their Entire Execution Owner
|
||||||
|
|
||||||
|
**Priority:** P3. Avoidable retained-memory dependency after use/discard.
|
||||||
|
|
||||||
|
**Code:** [PreparedExecution.owner](../../../internal/usecase/prepared_execution.go:24),
|
||||||
|
[Discard](../../../internal/usecase/prepared_execution.go:96),
|
||||||
|
[claim](../../../internal/usecase/prepared_execution.go:172), and
|
||||||
|
[payload clearing](../../../internal/usecase/prepared_execution.go:188).
|
||||||
|
|
||||||
|
**Circumstances:** A consumer retains a completed or discarded handle for its
|
||||||
|
supported `Details` access, while replacing or dropping the originating engine.
|
||||||
|
Payload clearing releases the per-execution plan, credential, observer, and
|
||||||
|
execution snapshot, but the handle's `owner *Runner` remains unchanged.
|
||||||
|
|
||||||
|
**Consequences / evidence:** The reference path is
|
||||||
|
`handle -> Runner -> captured repositories/clients/validator/capacity manager`.
|
||||||
|
Thus a small inert handle keeps the originating engine's complete runtime
|
||||||
|
snapshot and collaborators reachable, not just its deliberately retained details.
|
||||||
|
The offline diagnostic confirmed the same nonnil runner pointer after both
|
||||||
|
discard and successful execution. Retaining handles from successive engine
|
||||||
|
reconstructions can retain successive large snapshots. Keeping details itself
|
||||||
|
is required behavior, and this has no additional owner-retention cost while the
|
||||||
|
application still retains the same engine anyway.
|
||||||
|
|
||||||
|
**Potential remediation:** Clear the owner reference when the handle becomes
|
||||||
|
inert, checking ownership under the same mutex to avoid racing the current
|
||||||
|
unlocked owner read. Another option is a small standalone identity token shared
|
||||||
|
by a runner and its handles, which establishes ownership without retaining the
|
||||||
|
runner graph. Such a token must have reliable distinct identity; do not rely on
|
||||||
|
distinct zero-sized allocations comparing unequal. Preserve related namespace
|
||||||
|
views, foreign-owner rejection, single claim, discard/run races, and details
|
||||||
|
availability. No engine `Close` method or automatic closing of caller-owned
|
||||||
|
collaborators is implied.
|
||||||
|
|
||||||
|
## 4. Repair Feedback's Output Bound Does Not Bound Its Temporary Serialization
|
||||||
|
|
||||||
|
**Priority:** P3. Avoidable memory amplification on validation failures.
|
||||||
|
|
||||||
|
**Code:** [formatRepairDiagnostics](../../../internal/usecase/repairer.go:119).
|
||||||
|
|
||||||
|
**Circumstances:** A failed schema validation produces a large diagnostic or many
|
||||||
|
diagnostics and repair is enabled. The formatter normalizes every string, marshals
|
||||||
|
the complete array, then discovers that it exceeds 64 KiB. It subsequently
|
||||||
|
marshals individual full entries again before truncating one of them.
|
||||||
|
|
||||||
|
**Consequences:** Although the returned feedback is bounded correctly, temporary
|
||||||
|
storage scales with the complete diagnostic text, including text omitted from
|
||||||
|
the request. JSON escaping can amplify its size, and normalization adds another
|
||||||
|
copy for invalid UTF-8. Large validation messages and concurrent repairs can
|
||||||
|
therefore create large additional buffers despite producing tiny feedback.
|
||||||
|
The complete diagnostics already exist for the validation result; these extra
|
||||||
|
full encodings do not help preserve them. Existing formatting tests check output
|
||||||
|
bounds/data preservation, not scratch-space use.
|
||||||
|
|
||||||
|
**Potential remediation:** Encode feedback incrementally with bounded buffering
|
||||||
|
and lookahead sufficient to distinguish a complete array from an omitted suffix.
|
||||||
|
Stop encoding omitted text rather than allocating its full JSON representation.
|
||||||
|
Keep the existing 64 KiB result bound, ordered entries, rune-safe truncation,
|
||||||
|
valid UTF-8, JSON escaping, and in-bound omission notice. Leave the original
|
||||||
|
validation diagnostics and full previous candidate unchanged. This is an
|
||||||
|
implementation opportunity, not a claim that the existing contract promises a
|
||||||
|
64 KiB peak heap footprint or permission to add a diagnostic-input limit.
|
||||||
|
|
||||||
|
## 5. Short Normalized Session IDs Can Pin Large Input Strings
|
||||||
|
|
||||||
|
**Priority:** P3. Avoidable backing-buffer retention in retained metadata.
|
||||||
|
|
||||||
|
**Code:** [NormalizeSessionID](../../../internal/domain/session.go:10),
|
||||||
|
[preparation](../../../internal/usecase/runner.go:312), and
|
||||||
|
[prepared metadata construction](../../../internal/usecase/runner.go:491).
|
||||||
|
|
||||||
|
**Circumstances:** A direct or rendered session ID contains substantial leading
|
||||||
|
or trailing whitespace around a short valid identifier. `strings.TrimSpace`
|
||||||
|
returns a substring; normalization validates that substring and returns it
|
||||||
|
without detaching its backing storage. Public conversion preserves the string.
|
||||||
|
|
||||||
|
**Consequences / evidence:** An offline public-API probe prepared a request with
|
||||||
|
1 MiB of spaces followed by `s`. The returned session ID had length one but its
|
||||||
|
backing address still pointed inside the large original allocation. Retaining a
|
||||||
|
prepared value, handle details, or run result can therefore retain that buffer
|
||||||
|
after the original request/rendering buffer would otherwise be collectible.
|
||||||
|
The 256-code-point semantic limit does not prevent this retention. This does not
|
||||||
|
require changing which padded identifiers are accepted.
|
||||||
|
|
||||||
|
**Potential remediation:** Detach accepted normalized session metadata with a
|
||||||
|
small owned string, for example `strings.Clone` after validation. Preserve empty
|
||||||
|
omission, Unicode trimming/counting, hashes, and wire content. Review other
|
||||||
|
short canonical metadata returned by trimming for the same pattern when making
|
||||||
|
the change; do not indiscriminately clone large prompt/message payloads or add a
|
||||||
|
new raw-session-size restriction.
|
||||||
|
|
||||||
|
## Cleanup Checks And Non-Findings
|
||||||
|
|
||||||
|
No additional descriptor, timer, connection-ownership, or goroutine leak was
|
||||||
|
established. Artifact/schema handles use deferred closure; prompt content handles
|
||||||
|
close after reading; generation response bodies close on success and decoding,
|
||||||
|
status, and size failures. Diagnostic bodies close, and their timeout cancellation
|
||||||
|
is attached to body closure. Retry timers stop on exit. Admission/active permits
|
||||||
|
release on errors, cancellation, and panic unwinding. Prepared payloads otherwise
|
||||||
|
clear as intended.
|
||||||
|
|
||||||
|
Caller-selected artifact/schema/prompt sizes, intentionally unlimited backends,
|
||||||
|
and caller-retained ready handles are not classified as bugs merely because
|
||||||
|
their total memory can grow. Synchronous dependency calls may delay cancellation
|
||||||
|
by design, rather than being detached into leaking goroutines. Borrowed HTTP
|
||||||
|
transports/filesystems/clients remain subject to their ownership contracts; this
|
||||||
|
audit proposes no blanket shutdown or disposal API.
|
||||||
|
|
||||||
|
Previously recorded repeated-copying issues remain in
|
||||||
|
[the efficiency audit](02-efficiency.md); they are not duplicated here.
|
||||||
197
docs/roadmap/audit/06-error-handling.md
Normal file
197
docs/roadmap/audit/06-error-handling.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# Error-Handling Audit
|
||||||
|
|
||||||
|
Reviewed 2026-11-04 against commit `6b1323d`.
|
||||||
|
|
||||||
|
## Scope And Evidence
|
||||||
|
|
||||||
|
Reviewed error detection, wrapping, public translation, diagnostic reporting,
|
||||||
|
cancellation and timeout handling, retries, validation/repair failures, and
|
||||||
|
cleanup across construction, configuration inspection, prompt/profile selection,
|
||||||
|
artifact reading, rendering, ordinary and prepared execution, generation, and
|
||||||
|
smoke diagnostics, plus the small release-entrypoint wrappers. Contracts were
|
||||||
|
checked against GoDoc, integration references, the architecture policy, ADR 0001,
|
||||||
|
and existing tests.
|
||||||
|
|
||||||
|
Configuration composition belongs to ConfigKit; PromptKit validates and
|
||||||
|
interprets its final handoff and preserves upstream load errors. The runner adds
|
||||||
|
operation context, generation coordinates attempts and status retries, the HTTP
|
||||||
|
adapter classifies/redacts provider failures, and the public facade translates
|
||||||
|
internal categories. Content-validation failures and concluded diagnostic
|
||||||
|
failures are generally data rather than Go errors under their respective
|
||||||
|
contracts. Prepared handles permit one execution claim; failure after a
|
||||||
|
successful claim does not make the handle reusable.
|
||||||
|
|
||||||
|
Evidence includes source/call-graph review and a temporary public-API diagnostic
|
||||||
|
outside the repository. It used injected collaborators and a controlled HTTP
|
||||||
|
transport; it made no live network calls. No production code or repository
|
||||||
|
tests were changed.
|
||||||
|
|
||||||
|
Existing error, failure, cancellation, timeout, panic-cleanup, and retry tests
|
||||||
|
passed using:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test -count=1 -run 'Error|Failure|Fail|Cancel|Timeout|Panic|Release|Retry|Retries' \
|
||||||
|
. ./internal/llm ./internal/generation ./internal/usecase \
|
||||||
|
./internal/capacity ./internal/validate ./internal/artifact \
|
||||||
|
./internal/configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
These tests establish protected behavior but do not cover the three reproductions
|
||||||
|
below. Findings from the earlier correctness and resource-management audits are
|
||||||
|
not repeated here.
|
||||||
|
|
||||||
|
## 1. Nested Collaborator Errors Can Hide The Owning Operation's Public Category
|
||||||
|
|
||||||
|
**Priority:** P2. Confirmed public error-classification defect.
|
||||||
|
|
||||||
|
**Code:** [publicErrorFor](../../../errors.go:63),
|
||||||
|
[mapPublicError](../../../errors.go:16), and
|
||||||
|
[wrapGenerationError](../../../internal/usecase/runner.go:271).
|
||||||
|
|
||||||
|
**Failure condition:** An injected `LLMClient` or `ArtifactReader` returns an
|
||||||
|
error obtained from another PromptKit operation. Ordinary mapped errors retain
|
||||||
|
their internal chain. `publicErrorFor` searches that complete chain with
|
||||||
|
`errors.Is` and chooses the first matching category in a fixed priority order,
|
||||||
|
regardless of which operation the outer runner classified.
|
||||||
|
|
||||||
|
**Expected:** A model-client failure matches `ErrLLMGenerate`; an artifact-reader
|
||||||
|
failure matches `ErrArtifactLoad`. The injected cause remains available through
|
||||||
|
`errors.Is`, as their [public GoDoc](../../../engine.go:49) specifies. Preserving
|
||||||
|
the nested cause may also preserve its public categories; it must not replace
|
||||||
|
the category for the operation that just failed.
|
||||||
|
|
||||||
|
**Actual / reproduction:** A child engine's `Run` returns a missing-prompt error.
|
||||||
|
A parent engine selects a valid prompt, then its injected client returns that
|
||||||
|
child error. The outer error contains the text `failed to generate output`, but:
|
||||||
|
|
||||||
|
```text
|
||||||
|
errors.Is(err, ErrLLMGenerate) = false
|
||||||
|
errors.Is(err, ErrPromptNotFound) = true
|
||||||
|
errors.Is(err, childError) = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Returning the same child error from an injected artifact reader similarly
|
||||||
|
produces `errors.Is(err, ErrArtifactLoad) == false`. This is reproducible using
|
||||||
|
only public APIs, without importing internal sentinels. Consumers can therefore
|
||||||
|
miss the documented failure category when composing PromptKit-backed adapters.
|
||||||
|
|
||||||
|
**Potential remediation:** Translate the owning operation's classification
|
||||||
|
independently of an arbitrary collaborator's nested chain, while retaining the
|
||||||
|
original injected cause. Explicit boundary/stage information or mapping at the
|
||||||
|
operation boundary can do this. Merely reordering the global switch risks
|
||||||
|
breaking intentional direct-selection behavior: a genuinely missing prompt or
|
||||||
|
profile must retain its documented distinction from other load failures.
|
||||||
|
Protect nested-client and nested-reader cases through public-boundary regression
|
||||||
|
coverage; preserve existing capacity, retry-exhaustion, provider-error redaction,
|
||||||
|
and context identities.
|
||||||
|
|
||||||
|
## 2. Successful-Status Body Read Failures Are Misreported As Malformed JSON
|
||||||
|
|
||||||
|
**Priority:** P2. Confirmed loss of failure provenance and cause.
|
||||||
|
|
||||||
|
**Code:** [decodeOpenAIChatResponse](../../../internal/llm/openai_compatible_client.go:373).
|
||||||
|
|
||||||
|
**Failure condition:** A 2xx response body returns a non-EOF I/O error other than
|
||||||
|
`context.Canceled` or `context.DeadlineExceeded`, either during the first decode
|
||||||
|
or while checking the required final EOF. For example, a connection can fail
|
||||||
|
while reading the response stream after headers were received.
|
||||||
|
|
||||||
|
**Expected:** Distinguish an underlying read/transport failure from evidence of
|
||||||
|
invalid JSON or trailing data. Retain the read cause through a redacting error
|
||||||
|
so callers can inspect it with `errors.Is`/`errors.As`, consistent with the
|
||||||
|
[transport category](../../../engine.go:65). Do not report trailing data when the
|
||||||
|
reader failed without supplying any. This does not change the established
|
||||||
|
classification of invalid/truncated JSON or introduce transport retries.
|
||||||
|
|
||||||
|
**Actual / reproduction:** The controlled transport returned HTTP 200 and a body
|
||||||
|
that supplied either a partial JSON prefix or a complete valid chat envelope,
|
||||||
|
then returned a distinct read-error sentinel. Both executions returned
|
||||||
|
`ErrLLMGenerate` plus `ErrMalformedResponse`, did not match `ErrTransport`, and
|
||||||
|
discarded the sentinel. For the complete envelope, the error said `response
|
||||||
|
contains trailing data`, although no trailing bytes were supplied. Inference
|
||||||
|
smoke diagnostics likewise reported `malformed_response` with nil Go error
|
||||||
|
rather than a transport failure. Thus the issue affects both public execution
|
||||||
|
errors and factual diagnostic classification.
|
||||||
|
|
||||||
|
The integration contract explicitly guarantees exact causes for
|
||||||
|
[`http.Client.Do` failures](../../integrations/openai-compatible-chat.md#response-handling),
|
||||||
|
not every decoder error. The finding concerns the separate loss and
|
||||||
|
misclassification of an actual body-read failure, rather than asserting a new
|
||||||
|
requirement to expose JSON parser errors or provider content.
|
||||||
|
|
||||||
|
**Potential remediation:** Track underlying reader failures separately from
|
||||||
|
decoder/framing failures, or reliably distinguish those classes before replacing
|
||||||
|
the decode error. Reuse the redacting transport-error path for genuine read
|
||||||
|
failures. Keep syntax/type errors, EOF-truncated JSON, extra JSON values, and
|
||||||
|
size overflow in the existing malformed-response category; preserve cancellation
|
||||||
|
precedence and bounded reads/closure. Add focused coverage for interrupted
|
||||||
|
initial decoding and interrupted EOF verification, including diagnostic failure
|
||||||
|
classification. Default formatting must remain free of raw response content and
|
||||||
|
private transport details.
|
||||||
|
|
||||||
|
## 3. Inference Diagnostics Swallow Some Invalid Request-Construction Errors
|
||||||
|
|
||||||
|
**Priority:** P2. Confirmed mismatch with the diagnostic error contract.
|
||||||
|
|
||||||
|
**Code:** [runInference](../../../internal/usecase/smoke_tester.go:207),
|
||||||
|
[generation preparation](../../../internal/generation/coordinator.go:96), and
|
||||||
|
[request payload validation](../../../internal/llm/openai_compatible_client.go:511).
|
||||||
|
|
||||||
|
**Failure condition:** An inference diagnostic's effective settings are invalid
|
||||||
|
in a way detected by built-in request construction rather than target selection.
|
||||||
|
A runtime `Execution.ExtraParams` override containing the reserved key `model`
|
||||||
|
is one public example. The coordinator returns `llm.ErrInvalidRequest` before
|
||||||
|
dispatch with no attempt evidence. `runInference` treats every generation error
|
||||||
|
as a concluded diagnostic failure.
|
||||||
|
|
||||||
|
**Expected:** The [SmokeTest contract](../../../smoke_diagnostics.go:238) says invalid
|
||||||
|
kinds, settings, and selections return an ordinary Go error before diagnostic
|
||||||
|
work. A locally invalid request should follow that path and expose the public
|
||||||
|
invalid-request category. This is distinct from a provider rejecting a validly
|
||||||
|
constructed request or a diagnostic concluding with an observed failure.
|
||||||
|
|
||||||
|
**Actual / reproduction:** Calling text-inference `SmokeTest` with
|
||||||
|
`Execution.ExtraParams: map[string]any{"model": "override"}` returned nil Go
|
||||||
|
error and a report with `Completed == true`, `Failure == invalid_request`, no
|
||||||
|
attempts, and zero transport calls. The request-construction error and its
|
||||||
|
explanation were discarded. The caller cannot use the documented ordinary-error
|
||||||
|
path to identify this configuration mistake.
|
||||||
|
|
||||||
|
**Potential remediation:** Preserve the distinction between local built-in
|
||||||
|
request-preparation failures and concluded execution failures. Return preparation
|
||||||
|
errors through the use-case/public invalid-request mapping rather than marking
|
||||||
|
the diagnostic complete; preparation can remain in the generation adapter, with
|
||||||
|
enough provenance returned to the caller to make that distinction. Do not infer
|
||||||
|
that every zero-attempt diagnostic is an error: missing required credentials,
|
||||||
|
capacity rejection, unsupported operations, and inapplicability have intentional
|
||||||
|
report semantics. Preserve concluded provider failures and the separate
|
||||||
|
caller-cancellation behavior. Add one public-boundary regression for the
|
||||||
|
reserved-field override, retaining existing tests for those intentional reports.
|
||||||
|
|
||||||
|
## Reviewed Behavior Not Raised As Defects
|
||||||
|
|
||||||
|
- ConfigKit owns raw-source composition and errors before the accepted handoff;
|
||||||
|
no additional PromptKit raw-source validation is proposed.
|
||||||
|
- Non-2xx provider detail parsing is deliberately best-effort. Unreadable,
|
||||||
|
oversized, or unrecognized error bodies retain the received status without
|
||||||
|
exposing the raw body. That differs intentionally from decoding a 2xx body.
|
||||||
|
- Retries are limited to selected HTTP statuses. Transport failures, malformed
|
||||||
|
responses, and arbitrary injected-client failures are not automatically
|
||||||
|
retried; no expansion is needed for these findings.
|
||||||
|
- Completed output-validation failures are returned as validation results;
|
||||||
|
operational schema failures are errors. Repair failure propagation and
|
||||||
|
exhaustion retain that distinction.
|
||||||
|
- Caller cancellation, configured request timeouts, cancellation during capacity
|
||||||
|
acquisition/backoff, synchronous schema operations, and partial diagnostic
|
||||||
|
evidence have explicit, tested handling. Callback panics propagate while
|
||||||
|
deferred permit/admission cleanup still runs.
|
||||||
|
- Ignored SHA-256 writer errors cannot occur with the selected implementation.
|
||||||
|
Best-effort closure of read-only files and HTTP bodies does not imply a
|
||||||
|
persistence guarantee or require converting a completed read into a failure.
|
||||||
|
- Release wrappers use fail-fast shell execution and `exec` to propagate the
|
||||||
|
selected tool's status. Publication and recovery implementation belongs to
|
||||||
|
ReleaseKit; no release or publication operation was run for this audit.
|
||||||
|
|
||||||
|
No additional state-recovery or cancellation defect was substantiated in this
|
||||||
|
pass. The findings do not call for a new recovery subsystem, automatic replay of
|
||||||
|
possibly completed provider work, or changes to source ownership.
|
||||||
205
docs/roadmap/audit/07-security.md
Normal file
205
docs/roadmap/audit/07-security.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# Security Audit
|
||||||
|
|
||||||
|
Reviewed 2026-11-04 against commit `cf0bfd4` and the current worktree. Pending
|
||||||
|
relocations of earlier audit documents do not change the production baseline
|
||||||
|
and were left untouched.
|
||||||
|
|
||||||
|
## Scope, Architecture, And Threat Boundary
|
||||||
|
|
||||||
|
PromptKit is an in-process Go library with a public facade and internal
|
||||||
|
configuration, execution, transport, validation, and observation components.
|
||||||
|
ConfigKit owns source loading/composition; PromptKit interprets its final
|
||||||
|
handoff and captures immutable definitions. Execution reads caller-selected
|
||||||
|
artifacts, renders captured templates, resolves credentials, and contacts the
|
||||||
|
selected backend. Prepared handles privately retain one execution's settings.
|
||||||
|
Inspection and observation expose deliberately selected metadata.
|
||||||
|
|
||||||
|
Reviewed configuration and request validation, filesystem/resource selection,
|
||||||
|
template and JSON handling, credential flow, public formatting and reports,
|
||||||
|
HTTP requests/redirect handling, provider responses, schema loading, repair,
|
||||||
|
backend capacity, prepared ownership, and release-entrypoint wiring. The review
|
||||||
|
included interactions with the selected ConfigKit and JSON Schema dependencies
|
||||||
|
and an advisory scan of runtime packages.
|
||||||
|
|
||||||
|
The [architecture policy](../../policy/architecture.md#repository-and-consumer-boundary)
|
||||||
|
and [consumer security guidance](../../consumers/pkg-promptkit.md#protect-files-and-generated-data)
|
||||||
|
place application authorization, untrusted path/endpoint admission, payload-size
|
||||||
|
policy, and output disclosure with the consumer. Configuration sources,
|
||||||
|
templates, and injected Go collaborators are application-selected capabilities.
|
||||||
|
Upstream HTTP response bytes remain an external trust boundary. This review
|
||||||
|
does not treat ordinary file selection, runtime endpoint overrides, namespace
|
||||||
|
selection, or supported message roles as authorization mechanisms.
|
||||||
|
|
||||||
|
No production code or repository tests were changed, no secrets were collected,
|
||||||
|
and no external provider or forge was probed. Network activity was limited to
|
||||||
|
obtaining the public Go vulnerability checker/database and reading official
|
||||||
|
advisories. This is a source review with focused diagnostics, not a claim that
|
||||||
|
every possible consumer deployment is secure.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Source/call-graph review and the applicable public GoDoc, framework formats,
|
||||||
|
integration contract, policies, and ADR 0001.
|
||||||
|
- Existing focused tests passed for credential/report formatting, sensitive
|
||||||
|
configuration inspection/diffs, schema reference containment, provider-body
|
||||||
|
bounds, JSON value cycles/depth, prepared ownership, and capacity boundaries.
|
||||||
|
- A temporary offline public-API diagnostic used a controlled HTTP transport
|
||||||
|
and a synthetic credential. It verified the authentication-body behavior in
|
||||||
|
finding 2 and confirmed that credential was absent from the serialized report.
|
||||||
|
- `govulncheck` v1.9.0 scanned `./...` using Go 1.27.0 and the Go vulnerability
|
||||||
|
database last modified `2026-11-02T22:01:43Z`. The runtime scan covered the
|
||||||
|
module, its examples, ConfigKit v1.1.0, jsonschema/v6 v6.0.2, YAML v4.0.0-rc.6,
|
||||||
|
and x/text v0.14.0. It is not a complete audit of ReleaseKit's implementation
|
||||||
|
or every dependency used only by the release tool.
|
||||||
|
|
||||||
|
The text-mode scan reported ten symbol-level standard-library advisories, plus
|
||||||
|
package/module-only matches. Those are candidate matches, not ten established
|
||||||
|
PromptKit vulnerabilities. Applicability was checked against the actual
|
||||||
|
operations described below rather than inferred from interface-call traces.
|
||||||
|
|
||||||
|
## 1. The Audited Go Toolchain Predates Applicable Networking Security Fixes
|
||||||
|
|
||||||
|
**Priority:** P2. Build/deployment security finding, not a newly discovered
|
||||||
|
PromptKit algorithm defect.
|
||||||
|
|
||||||
|
**Relevant code:** [declared Go minimum](../../../go.mod:3),
|
||||||
|
[default HTTP-client construction](../../../internal/llm/openai_compatible_client.go:58),
|
||||||
|
[generation dispatch](../../../internal/llm/openai_compatible_client.go:211), and
|
||||||
|
[diagnostic dispatch](../../../internal/llm/http_evidence.go:129).
|
||||||
|
|
||||||
|
**Condition and impact:** The local toolchain used for this audit is Go 1.27.0.
|
||||||
|
Applications built with that patch can exercise affected standard-library
|
||||||
|
networking through PromptKit's normal outbound client:
|
||||||
|
|
||||||
|
- [GO-2026-6611](https://pkg.go.dev/vuln/GO-2026-6611) describes excessive CPU
|
||||||
|
consumption caused by a malicious HTTP/2 peer repeatedly changing initial
|
||||||
|
stream windows. An application using HTTP/2 against a hostile or compromised
|
||||||
|
backend, with enough active streams for amplification, can suffer availability
|
||||||
|
loss. The Go 1.27 fix is in 1.27.2. Body-size limits do not address protocol
|
||||||
|
processing below response decoding.
|
||||||
|
- [GO-2026-6809](https://pkg.go.dev/vuln/GO-2026-6809) adds explicit rejection of
|
||||||
|
compromised intermediate certificates. The affected conditions depend on the
|
||||||
|
platform and trust-store state. When the old trust path remains accepted, an
|
||||||
|
attacker able to present a matching certificate chain and intercept the
|
||||||
|
connection could compromise confidentiality/integrity of prompts, responses,
|
||||||
|
or bearer credentials. The Go 1.27 fix is in 1.27.3. This is not a claim that
|
||||||
|
every default system trust store remains affected.
|
||||||
|
|
||||||
|
**Evidence and limits:** The advisories establish affected versions and attack
|
||||||
|
conditions; source review establishes use of the standard outbound HTTP/TLS
|
||||||
|
path. No certificate forgery, HTTP/2 attack, or running consumer binary was
|
||||||
|
tested. The scanner's surprising interface-based paths from unrelated methods
|
||||||
|
are not relied upon as proof of exploitation. PromptKit is source-only: a
|
||||||
|
consumer's actual compiled toolchain and transport configuration determine its
|
||||||
|
exposure. The `go 1.27.0` directive is a compatibility minimum, not a record of
|
||||||
|
which patch built an application.
|
||||||
|
|
||||||
|
**Potential remediation:** Use an appropriate current patched Go toolchain,
|
||||||
|
at least 1.27.3 within this series for these two fixes, for release validation
|
||||||
|
and ReleaseKit execution; advise consumers to rebuild affected applications
|
||||||
|
with patched Go. Recheck advisories against that selected toolchain. This fits
|
||||||
|
the existing distinction between consumer minimum and maintainer toolchain in
|
||||||
|
the [release procedure](../../release.md#prepare-the-candidate).
|
||||||
|
Whether to raise the consumer minimum for the identified fixes is a separate
|
||||||
|
compatibility decision under the existing policy, not an automatic consequence
|
||||||
|
of this audit. These fixes do not require custom TLS verification, a blanket
|
||||||
|
HTTP/2 ban, or a new recurring release gate.
|
||||||
|
|
||||||
|
## 2. Authentication Probes Permit Unbounded Remote-Body Memory Consumption
|
||||||
|
|
||||||
|
**Priority:** P2. Existing availability finding, confirmed at the external
|
||||||
|
response boundary. This is the same mechanism as resource-management audit
|
||||||
|
finding 1, not a second independent remediation item.
|
||||||
|
|
||||||
|
**Relevant code:** [authentication recognition](../../../internal/usecase/smoke_authentication.go:139),
|
||||||
|
[authentication probe execution](../../../internal/usecase/smoke_authentication.go:66),
|
||||||
|
and [diagnostic HTTP execution](../../../internal/llm/http_evidence.go:129).
|
||||||
|
|
||||||
|
**Condition:** A consumer explicitly invokes an `openrouter_key` authentication
|
||||||
|
diagnostic. The selected endpoint, a compromised provider, or an intermediary
|
||||||
|
on an operator-selected plaintext connection returns HTTP 200 with a large or
|
||||||
|
continuously growing body. Recognition uses unrestricted `io.ReadAll` followed
|
||||||
|
by whole-document JSON unmarshalling. Unlike generation response decoding,
|
||||||
|
this path has no byte budget. HTTP-only probes also do not use generation
|
||||||
|
capacity admission.
|
||||||
|
|
||||||
|
**Impact:** The remote peer controls memory consumption before the diagnostic
|
||||||
|
returns. A fast response can cause large allocations or process termination
|
||||||
|
before a deadline, and concurrent probes multiply the exposure. A configured
|
||||||
|
timeout bounds time, not bytes allocated during that interval. This requires a
|
||||||
|
diagnostic call; it is not an unauthenticated inbound attack on a PromptKit
|
||||||
|
server, and ordinary `Run` does not execute the probe.
|
||||||
|
|
||||||
|
**Evidence:** The offline diagnostic returned a minimal recognized account
|
||||||
|
object and 4 MiB of irrelevant padding. PromptKit read all 4,194,374 bytes,
|
||||||
|
closed the body, and concluded with credential acceptance. The body used an
|
||||||
|
unknown content length. No deliberate memory exhaustion was attempted. The
|
||||||
|
synthetic bearer credential was supplied to the controlled transport and was
|
||||||
|
absent from the serialized diagnostic report.
|
||||||
|
|
||||||
|
**Potential remediation:** Select a finite body budget for this small diagnostic
|
||||||
|
protocol and enforce it for declared and streamed lengths, closing on overflow
|
||||||
|
without unbounded draining. Preserve factual reporting: overflow establishes
|
||||||
|
neither credential rejection nor acceptance. Agree the limit and overflow
|
||||||
|
classification explicitly; no existing authentication-body byte guarantee is
|
||||||
|
being enforced. The existing resource-audit remediation can own this fix;
|
||||||
|
there is no need for separate global limits on prompts or artifacts.
|
||||||
|
|
||||||
|
## Other Candidates And Boundary Checks
|
||||||
|
|
||||||
|
- **Scanner applicability:** Server-only advisories GO-2026-6603, 6612, 6613,
|
||||||
|
and 6617 do not establish an attack on PromptKit's outbound client. ECH
|
||||||
|
server parsing (6607), multipart-form parsing (6608), and Range-serving
|
||||||
|
behavior (6609) likewise lack a matching operation here. The CONNECT-with-body
|
||||||
|
condition in 6605 and raw HTTP/2-to-HTTP/1 header forwarding in 6610 are absent
|
||||||
|
from built-in POST/GET generation and factual diagnostic reporting. Windows
|
||||||
|
junction-directory creation (6604) is not established by read-only resource
|
||||||
|
opens. These exclusions concern PromptKit, not all possible code in a host
|
||||||
|
application.
|
||||||
|
- **Module-only matches:** x/text advisories GO-2026-5970 and 6629 were not
|
||||||
|
reported as called vulnerable symbols; no matching use of `norm.Iter` or
|
||||||
|
`precis.Nickname` was established. html/template advisories 6599 and 6600 do
|
||||||
|
not apply to the text/template renderer. No separate runtime third-party
|
||||||
|
vulnerability was substantiated by this scan; that is not a general safety
|
||||||
|
certification of those modules.
|
||||||
|
- **Files and schemas:** Ordinary artifacts intentionally allow unrestricted
|
||||||
|
caller-selected regular files. Host-backed `content_file` contexts use
|
||||||
|
ConfigKit's rooted filesystem, while the standard schema loader checks
|
||||||
|
canonical containment and rejects remote reference schemes. Generic `fs.FS`
|
||||||
|
inputs remain caller-supplied capabilities; logical path checks do not turn an
|
||||||
|
arbitrary filesystem implementation into a host sandbox. No additional
|
||||||
|
confinement guarantee is proposed.
|
||||||
|
- **Injection:** Variables and artifact text are rendered as data rather than
|
||||||
|
reparsed as Go templates. Captured templates have the input helper and normal
|
||||||
|
text/template functionality, not a shell or process-execution helper.
|
||||||
|
Provider payloads use JSON encoding and reserved-field checks. Model output
|
||||||
|
is data for validation/repair, not executable code or an authorization
|
||||||
|
decision. Prompt injection and domain-aware interpretation remain consumer
|
||||||
|
concerns.
|
||||||
|
- **Credentials and disclosure:** Direct keys are request-scoped, excluded from
|
||||||
|
supported JSON and public request summaries, and removed from diagnostic
|
||||||
|
settings and prepared details. Provider error defaults redact details;
|
||||||
|
explicit provider accessors and arbitrary application content retain the
|
||||||
|
documented consumer disclosure responsibility. Inspection/diff tests protect
|
||||||
|
configured sensitive provider paths. No blanket redaction of arbitrary
|
||||||
|
prompt, schema, variable, error, or provider content is promised.
|
||||||
|
- **Ownership and availability:** Engine snapshots and copied public values
|
||||||
|
prevent ordinary cross-call mutation. Prepared handles enforce owner binding
|
||||||
|
and single claim. Capacity and retry cleanup are tested. Intentionally
|
||||||
|
unlimited backends, synchronous template/schema dependency calls, and
|
||||||
|
application-selected large files are not newly classified as vulnerabilities.
|
||||||
|
The other resource-audit findings remain maintenance items rather than
|
||||||
|
independently demonstrated remote attacks.
|
||||||
|
- **Release tooling:** Local launchers pass quoted arguments through `exec`;
|
||||||
|
there is no PromptKit runtime command-execution surface. The delivery token
|
||||||
|
path is ignored and selected for committed-file protection. Publication,
|
||||||
|
token-file permission enforcement, and recovery implementation belong to
|
||||||
|
ReleaseKit; no publication command was run during this audit.
|
||||||
|
|
||||||
|
## Recommended Follow-Up
|
||||||
|
|
||||||
|
Use a patched Go toolchain before release preparation and address the existing
|
||||||
|
authentication-body availability finding. Neither requires a new application
|
||||||
|
authorization subsystem or stronger filesystem, endpoint, or semantic-output
|
||||||
|
contracts. No additional PromptKit-specific confidentiality, integrity, or
|
||||||
|
authorization defect was substantiated within the documented boundary.
|
||||||
284
docs/roadmap/audit/08-testing.md
Normal file
284
docs/roadmap/audit/08-testing.md
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# Test Coverage And Effectiveness Audit
|
||||||
|
|
||||||
|
Reviewed 2026-11-04 against commit `f7085d7` and the current worktree. Existing
|
||||||
|
audit-document moves were preserved. No production code or tests were changed.
|
||||||
|
|
||||||
|
## Assessment And Method
|
||||||
|
|
||||||
|
The suite broadly follows the [testing policy](../../policy/testing.md): real
|
||||||
|
ConfigKit composition, filesystems, rendering, validation, and engine assembly
|
||||||
|
are exercised alongside controlled model clients and HTTP transports. Internal
|
||||||
|
tests own focused rules and coordination, external-package root tests protect
|
||||||
|
the public facade, and the three maintained examples have Go workflow tests.
|
||||||
|
Capacity, prepared-handle state transitions, ownership, cancellation, retries,
|
||||||
|
repair, observations, and configuration inspection have substantial behavioral
|
||||||
|
coverage. This is a targeted improvement list, not a recommendation to replace
|
||||||
|
the test architecture or pursue a percentage threshold.
|
||||||
|
|
||||||
|
Both required analyses were performed: review of missing behavioral protection,
|
||||||
|
and review of assertion/fixture cost and coupling. Evidence includes source and
|
||||||
|
call-graph review, existing GoDoc and integration contracts, full-suite coverage,
|
||||||
|
repeated race-enabled execution, and reuse of the earlier error audit's temporary
|
||||||
|
offline diagnostic. No live provider, paid API, new repository test, production
|
||||||
|
mutation, or external mutation-testing service was needed.
|
||||||
|
|
||||||
|
Validation used the local Go 1.27.0 toolchain:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test -count=1 -covermode=atomic -coverpkg=./... -coverprofile=/tmp/promptkit-testing-audit-LHAyNm/coverage.out ./...
|
||||||
|
go tool cover -func=/tmp/promptkit-testing-audit-LHAyNm/coverage.out
|
||||||
|
go test -race -vet=off -shuffle=42 -count=2 ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
Both suite runs passed, including the examples. The race run exercised two
|
||||||
|
shuffled passes; this is evidence against readily reproducible order/race
|
||||||
|
problems, not proof that no flakes exist. Coverage output remains outside the
|
||||||
|
repository. The commands are audit evidence, not additional release gates.
|
||||||
|
|
||||||
|
Combined coverage is **86.4%**, or 4,782 of 5,536 instrumented statements.
|
||||||
|
Selected package totals below combine hits across all test binaries. The
|
||||||
|
per-binary percentages printed by `-coverpkg=./...` are not individual-package
|
||||||
|
coverage percentages; repeated profile spans were combined before calculating
|
||||||
|
these totals.
|
||||||
|
|
||||||
|
| Component | Combined statement coverage |
|
||||||
|
| --- | ---: |
|
||||||
|
| Public facade | 81.0% |
|
||||||
|
| Configuration | 91.0% |
|
||||||
|
| Domain | 98.0% |
|
||||||
|
| Capacity | 98.0% |
|
||||||
|
| Generation coordination | 91.2% |
|
||||||
|
| HTTP model client | 94.2% |
|
||||||
|
| Execution use cases | 90.5% |
|
||||||
|
| Rendering and requirement inspection | 78.4% |
|
||||||
|
| Output/schema validation | 75.8% |
|
||||||
|
|
||||||
|
Coverage located candidates; the findings below concern specific risks and
|
||||||
|
assertions, not the percentages themselves. Testing recommendations associated
|
||||||
|
with already recorded implementation defects should accompany those fixes,
|
||||||
|
rather than becoming duplicate remediation projects.
|
||||||
|
|
||||||
|
## 1. RunRequest's Privacy Formatting Contract Is Not Exercised
|
||||||
|
|
||||||
|
**Priority:** P2. Missing protection for a documented confidentiality boundary.
|
||||||
|
|
||||||
|
**Relevant code:** [RunRequest formatting](../../../formatting.go:5) and the
|
||||||
|
analogous existing [SmokeTestRequest privacy test](../../../smoke_test_contract_test.go:135).
|
||||||
|
|
||||||
|
**Deficiency:** `RunRequest.String`, `GoString`, and their shared formatter all
|
||||||
|
have 0% combined statement coverage. The suite protects formatting of generated
|
||||||
|
client requests, smoke-test requests, and opaque prepared handles, but does not
|
||||||
|
exercise the ordinary run request's own formatting contract.
|
||||||
|
|
||||||
|
**Unverified behavior:** A change that prints the direct key, input bodies, or
|
||||||
|
variable values from `RunRequest` can pass the complete suite. This is an
|
||||||
|
assertion gap, not evidence that current formatting actually leaks those values.
|
||||||
|
|
||||||
|
**Potential improvement:** Add one small public-boundary test with distinct
|
||||||
|
synthetic private markers and exercise the supported String/GoString formatting
|
||||||
|
paths on values and pointers. Assert absence of the data covered by the GoDoc;
|
||||||
|
do not snapshot the complete summary or freeze incidental field order/count
|
||||||
|
formatting. If adding a JSON check, assert only the documented direct-key
|
||||||
|
exclusion, not a stable JSON shape or omission of ordinary request data.
|
||||||
|
Reflection-based disclosure is explicitly outside this formatter guarantee.
|
||||||
|
|
||||||
|
## 2. The Public Single-Schema-File Option Has No Execution Coverage
|
||||||
|
|
||||||
|
**Priority:** P2. An exported source-selection path is entirely unprotected.
|
||||||
|
|
||||||
|
**Relevant code:** [WithSchemaFile](../../../engine.go:212),
|
||||||
|
[SetSchemaFile](../../../internal/engine/engine.go:75), and
|
||||||
|
[fileSource](../../../internal/engine/engine.go:211).
|
||||||
|
|
||||||
|
**Deficiency:** All three functions have 0% combined coverage. The existing
|
||||||
|
[single-file validator test](../../../internal/validate/standard_validator_test.go:596)
|
||||||
|
constructs `FSValidator` directly with `fstest.MapFS`; it does not test the public
|
||||||
|
option, host-file selection, or its engine wiring.
|
||||||
|
|
||||||
|
**Unverified behavior:** The public option could ignore its argument, fail to
|
||||||
|
replace `SchemaDir`, select the wrong root/base name, or mishandle invalid files
|
||||||
|
without this validator test noticing. Direct validator success does not establish
|
||||||
|
that a consumer can reach that behavior through the supported API.
|
||||||
|
|
||||||
|
**Potential improvement:** Use `t.TempDir` and a real schema file in a small
|
||||||
|
public integration test. Exercise construction plus preparation/validation
|
||||||
|
through that file's base name, and representative invalid selections such as a
|
||||||
|
missing file or directory. Include source-option replacement where it protects
|
||||||
|
the documented last-valid-option behavior. Assert public error identity and
|
||||||
|
observable schema selection, not private helper calls. This does not require a
|
||||||
|
helper-by-helper test matrix or model-provider access.
|
||||||
|
|
||||||
|
## 3. Programmatic Provider-Value Conversion Lacks Representative Value Cases
|
||||||
|
|
||||||
|
**Priority:** P2. Conversion and ownership coverage misses known data-integrity
|
||||||
|
and deferred-error risks.
|
||||||
|
|
||||||
|
**Relevant tests:** [programmatic source ownership/order](../../../sources_contract_test.go:14)
|
||||||
|
and [programmatic construction](../../../construction_contract_test.go:226).
|
||||||
|
**Implementation:** [providerValueForMapping](../../../provider_values.go:31).
|
||||||
|
|
||||||
|
**Deficiency:** The converter has only 11.9% combined coverage. The principal
|
||||||
|
source-ownership test mutates a flat string-valued map. It does not establish
|
||||||
|
preservation and ownership for representative nested/typed containers or the
|
||||||
|
`json.Number` branch, nor deferred failure for unsupported provider values.
|
||||||
|
The more extensive `internal/jsonvalue` tests protect a different traversal and
|
||||||
|
do not execute this pre-ConfigKit convenience conversion.
|
||||||
|
|
||||||
|
**Unverified behavior:** The suite passes with the exact-number rounding defect
|
||||||
|
already recorded in [the correctness audit](01-correctness.md#1-provider-source-helpers-silently-change-exact-json-numbers).
|
||||||
|
It also would not detect many regressions in nested source capture or the
|
||||||
|
documented source-creation-versus-loading failure boundary.
|
||||||
|
|
||||||
|
**Potential improvement:** Exercise both `ProfileSource` and `BackendSource`
|
||||||
|
through `NewEngine` with a compact set of representative supported nested
|
||||||
|
values, mutations after source creation, and unsupported/cyclic values that
|
||||||
|
should fail during loading. Add the numeric regression when addressing the
|
||||||
|
existing precision finding. Compare meaningful final values or encoding, not Go
|
||||||
|
container identity, numeric spelling, or a required float representation.
|
||||||
|
Keep tests at the final handoff/public source-helper boundary; do not duplicate
|
||||||
|
ConfigKit's raw-source validation or its entire value-limit suite. Work-bound
|
||||||
|
coverage for the separately identified shared-graph issue belongs with that fix.
|
||||||
|
|
||||||
|
## 4. Error Tests Miss Composed Causes And Pre-Dispatch Diagnostic Failures
|
||||||
|
|
||||||
|
**Priority:** P2. Covered code paths lack important boundary-case assertions.
|
||||||
|
|
||||||
|
**Relevant tests:** [public generation failures](../../../generation_error_contract_test.go:111),
|
||||||
|
[direct mapper cases](../../../errors_internal_test.go:13),
|
||||||
|
[body cancellation](../../../internal/llm/prepared_generation_test.go:161), and
|
||||||
|
[inference diagnostic cases](../../../internal/usecase/smoke_inference_test.go:14).
|
||||||
|
|
||||||
|
**Deficiency and demonstrated blind spots:**
|
||||||
|
|
||||||
|
- Injected failures are mostly plain marker errors or a single explicit
|
||||||
|
category. They do not check a real nested PromptKit error returned through a
|
||||||
|
different operation. The suite passes although an injected client/reader can
|
||||||
|
lose the outer public generation/artifact category.
|
||||||
|
- Tests cover `http.Client.Do` failures and cancellation during successful-body
|
||||||
|
reading, but not an ordinary non-EOF read error during initial decoding or
|
||||||
|
final EOF verification. Such errors currently lose their cause and become
|
||||||
|
malformed-response failures.
|
||||||
|
- Diagnostic tests cover invalid selection/settings and concluded generation
|
||||||
|
failures separately, but miss a runtime setting rejected by built-in request
|
||||||
|
construction before dispatch. A reserved `ExtraParams["model"]` override
|
||||||
|
currently returns a completed report with nil Go error.
|
||||||
|
|
||||||
|
These are the three implementation findings in the error-handling audit, not
|
||||||
|
new duplicate defects. Re-running its temporary offline public-API diagnostic
|
||||||
|
against the current tree confirmed all three while the complete suite passed.
|
||||||
|
|
||||||
|
**Potential improvement:** Add narrowly owned regressions with those fixes:
|
||||||
|
real nested public causes through the injection boundary; a controlled body
|
||||||
|
reader failing at each decode/framing phase; and a public `SmokeTest` call with
|
||||||
|
a reserved runtime field and counted transport. Assert relevant error identities,
|
||||||
|
cause preservation, factual failure classification, and whether dispatch occurred.
|
||||||
|
Avoid reproducing the internal error topology by hand or matching complete
|
||||||
|
messages. Preserve intentional nil-error diagnostic findings, cancellation
|
||||||
|
precedence, best-effort non-2xx detail parsing, and status-only retry policy.
|
||||||
|
|
||||||
|
## 5. Multi-Resource Schema Tests Do Not Establish A Usable Provider Constraint
|
||||||
|
|
||||||
|
**Priority:** P2. Missing assembled validation-to-wire assertion.
|
||||||
|
|
||||||
|
**Relevant tests:** [prepared execution source freezing](../../../prepared_execution_contract_test.go:19),
|
||||||
|
its [referenced schema fixture](../../../prepared_execution_contract_test.go:809),
|
||||||
|
and [local reference validation](../../../internal/validate/standard_validator_test.go:627).
|
||||||
|
|
||||||
|
**Deficiency:** Local tests establish that referenced schemas compile and
|
||||||
|
validate. The prepared-execution test checks that the injected client's schema
|
||||||
|
matches frozen details. Its opaque model client accepts that schema without
|
||||||
|
interpreting references. Equality between those two values does not establish
|
||||||
|
that the provider-facing constraint contains the resources it needs.
|
||||||
|
|
||||||
|
**Unverified behavior:** Both sides can contain the same unresolved `value.json`
|
||||||
|
reference while the test passes. This is the packaging gap already identified
|
||||||
|
in [the correctness audit](01-correctness.md#2-external-schema-resources-do-not-reach-the-provider-constraint).
|
||||||
|
The freezing test's fake is appropriate for its stated ownership purpose; the
|
||||||
|
missing protection is a separate built-in-path integration case.
|
||||||
|
|
||||||
|
**Potential improvement:** Once the schema-packaging policy is selected, add one
|
||||||
|
offline assembled test with a root and child schema, capture the actual built-in
|
||||||
|
HTTP payload, and assess the chosen provider-facing representation independently
|
||||||
|
of `Details`. For a bundled native schema, compile/evaluate the captured
|
||||||
|
constraint without granting access to PromptKit's original local files; if the
|
||||||
|
chosen policy supports a local-only fallback, assert that behavior instead.
|
||||||
|
Keep the existing freezing test. Do not require live provider acceptance or
|
||||||
|
promise support for arbitrary provider schema dialect features.
|
||||||
|
|
||||||
|
## 6. Artifact Tests And Benchmarks Pin An Explicitly Opaque Hash Algorithm
|
||||||
|
|
||||||
|
**Priority:** P3. Remove or rewrite change-detector assertions, retaining the
|
||||||
|
substantive file-reading tests.
|
||||||
|
|
||||||
|
**Relevant assertions:** [size-hint test](../../../internal/artifact/reader_test.go:287)
|
||||||
|
and [benchmark correctness check](../../../internal/artifact/reader_benchmark_test.go:156).
|
||||||
|
|
||||||
|
**Deficiency:** Both independently calculate a hex SHA-256 digest and require
|
||||||
|
that exact value. The [public Artifact.Hash contract](../../../types.go:290)
|
||||||
|
expressly leaves its format and algorithm unspecified. A legitimate algorithm
|
||||||
|
or encoding change would fail tests unrelated to the requested behavior.
|
||||||
|
|
||||||
|
**Potential improvement:** Replace those algorithm-specific checks with the
|
||||||
|
opaque equality relationships needed for each test. For the under/overreported
|
||||||
|
size cases, comparing with the inline reader for the same complete body still
|
||||||
|
detects hashing only a reported-size prefix. The existing
|
||||||
|
[source-parity test](../../../internal/artifact/reader_test.go:27) already owns
|
||||||
|
repeatability, same-content equality, and changed-content inequality. Retain
|
||||||
|
body, actual-size, metadata, and read-boundary assertions. The benchmark may
|
||||||
|
reuse an opaque expected hash prepared outside measurement or rely on the
|
||||||
|
owning behavioral test; it need not maintain its own SHA-256 contract.
|
||||||
|
|
||||||
|
## 7. Bundled Catalog Expectations Have Multiple Test Owners
|
||||||
|
|
||||||
|
**Priority:** P3. Consolidation/removal candidate for duplicated assertions.
|
||||||
|
|
||||||
|
**Relevant tests:** [internal complete catalog table](../../../internal/configuration/bundled_test.go:12),
|
||||||
|
[public option/inventory table](../../../bundled_openrouter_contract_test.go:54),
|
||||||
|
[HTTP payload matrix](../../../bundled_execution_contract_test.go:51), and
|
||||||
|
[bundled smoke defaults](../../../bundled_smoke_contract_test.go:16).
|
||||||
|
|
||||||
|
**Deficiency:** The current count of 17, complete ID inventory, selected
|
||||||
|
ID-to-model mappings, tier-plugin data, and diagnostic default model recur
|
||||||
|
across configuration, facade, wire, and diagnostic tests. A catalog update can
|
||||||
|
require expectation edits in several layers even when loading, option handling,
|
||||||
|
routing, and serialization are unchanged. The internal plugin assertion also
|
||||||
|
searches `fmt.Sprint` output instead of comparing the plugin's structure.
|
||||||
|
|
||||||
|
**Potential improvement:** Choose one owner for the complete maintained asset
|
||||||
|
inventory and expected model/plugin/default data. Remove duplicate full-catalog
|
||||||
|
counts/lists from option permutations and keep those tests focused on enable,
|
||||||
|
disable, last-option selection, isolation, and override behavior. Retain a small
|
||||||
|
assembled wire/diagnostic set for distinct integration risks; controlled
|
||||||
|
overrides can isolate routing assertions from a changing catalog default.
|
||||||
|
Use semantic structural assertions for plugin data. Keep independent expected
|
||||||
|
asset values in the selected owner rather than deriving every expectation
|
||||||
|
from the production loader, which could conceal an omitted or incorrect asset.
|
||||||
|
This is consolidation, not a proposal to stop checking bundled configuration.
|
||||||
|
|
||||||
|
## Findings Not Supported And Work To Preserve
|
||||||
|
|
||||||
|
- No general test-flakiness, default-suite live-provider dependency, or race
|
||||||
|
failure was reproduced. Bounded timeout guards and channel synchronization
|
||||||
|
are not classified as flaky merely because a test involves concurrency.
|
||||||
|
- The public API/type-boundary checks and their checker fixtures protect an
|
||||||
|
explicit architectural rule; they are not automatically obsolete because
|
||||||
|
they inspect structure. Stable JSON shape tests protect selected contracts.
|
||||||
|
- Fakes at model/HTTP boundaries, counted paid-effect calls, and no-work-after-
|
||||||
|
rejection checks are appropriate. A fake is not deficient merely because it
|
||||||
|
does not model unrelated provider behavior.
|
||||||
|
- Exhaustive domain-rule tables plus representative boundary wiring tests are
|
||||||
|
generally sound. No wholesale deletion of error, cancellation, capacity,
|
||||||
|
ownership, or prepared-lifecycle coverage is recommended.
|
||||||
|
- Low coverage of thin wrappers, example `main` functions, defensive impossible
|
||||||
|
branches, and unexercised reflection variants is not independently a finding.
|
||||||
|
Interface-only packages do not need artificial tests to obtain a package
|
||||||
|
percentage. Conservative requirement inspection is not obliged to fully
|
||||||
|
evaluate unsupported template expressions just to cover every AST branch.
|
||||||
|
- The standalone release-wiring fixture remains scoped to local disposable
|
||||||
|
repositories under the documented testing model. This audit does not propose
|
||||||
|
moving it into every normal test run or retesting ReleaseKit's algorithms.
|
||||||
|
|
||||||
|
Prioritize the missing public privacy/source-option protection and regressions
|
||||||
|
for the already identified defects. Make the two assertion-cleanup changes as
|
||||||
|
focused rewrites, retaining their useful behavioral coverage. None of these
|
||||||
|
recommendations requires new public guarantees or a coverage-percentage gate.
|
||||||
199
docs/roadmap/audit/09-interfaces.md
Normal file
199
docs/roadmap/audit/09-interfaces.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# API And Interface Contract Audit
|
||||||
|
|
||||||
|
Reviewed 2026-11-04 against commit `31f1e6a` and the current worktree. Existing
|
||||||
|
audit-document moves were preserved. No production code or repository tests
|
||||||
|
were changed.
|
||||||
|
|
||||||
|
## Scope And Assessment
|
||||||
|
|
||||||
|
The public root package remains a facade over configuration, execution,
|
||||||
|
generation, capacity, rendering, validation, and observation components.
|
||||||
|
ConfigKit owns composition before the final configuration handoff; PromptKit
|
||||||
|
validates that handoff and captures its runtime indexes and templates. Public
|
||||||
|
adapters translate owned request/result values and errors. Namespace views
|
||||||
|
share execution ownership and capacity, while separately constructed engines
|
||||||
|
remain independent.
|
||||||
|
|
||||||
|
The review compared exported GoDoc, configuration and integration contracts,
|
||||||
|
internal collaborator interfaces, call paths, and existing behavioral tests.
|
||||||
|
It covered preparation/preflight/execution parity, prepared-handle lifecycle,
|
||||||
|
optional values and numeric presence, configuration inspection and listing,
|
||||||
|
injected collaborator ownership, output repair, diagnostics, observations,
|
||||||
|
serialization, cancellation, and public error categories. It did not treat
|
||||||
|
different intentionally documented workflows as defects or infer additional
|
||||||
|
admission, filesystem, provider, or semantic-validation guarantees.
|
||||||
|
|
||||||
|
Four findings warrant remediation. The configuration behaviors in findings 2
|
||||||
|
and 3 are consistent with the current format reference and implementation;
|
||||||
|
their older contradictory descriptions should be corrected rather than used
|
||||||
|
to change those behaviors.
|
||||||
|
|
||||||
|
## 1. ArtifactReader Permits Buffer Reuse Before PromptKit Can Copy It
|
||||||
|
|
||||||
|
**Priority:** P2. A documented ownership permission permits data races and
|
||||||
|
inconsistent input snapshots.
|
||||||
|
|
||||||
|
**Relevant interfaces/code:** [ArtifactReader GoDoc](../../../types.go:294),
|
||||||
|
[public reader adapter](../../../artifact_reader.go:19), and
|
||||||
|
[copyBytes](../../../convert.go:490).
|
||||||
|
|
||||||
|
**Contract defect:** The interface says that `Read` may be called concurrently
|
||||||
|
and that the reader may reuse or mutate the returned artifact and body after
|
||||||
|
`Read` returns. The adapter can only copy those values after that return.
|
||||||
|
There is no synchronization or acknowledgement between return and completion
|
||||||
|
of the copy. “Immediately copies” does not make concurrent reuse safe.
|
||||||
|
|
||||||
|
**Circumstances and consequence:** A reader serializes its own `Read` calls
|
||||||
|
with a mutex and returns a reusable buffer. Another invocation can overwrite
|
||||||
|
that buffer as soon as the first call returns, while the first adapter is still
|
||||||
|
copying it. Both calls follow the published reuse permission, yet preparation
|
||||||
|
can race, mix contents, or associate metadata with the wrong bytes. This is
|
||||||
|
not a demonstrated defect in the default file reader or in readers that
|
||||||
|
return independent snapshots.
|
||||||
|
|
||||||
|
**Evidence:** A temporary, offline public-API diagnostic used two concurrent
|
||||||
|
`Prepare` calls and a mutex-protected reader returning a shared 1 MiB buffer.
|
||||||
|
`go run -race` reported a write in the next `Read` racing with `copyBytes`
|
||||||
|
through `publicArtifactReaderAdapter.Read`. The adapter's existing copy test
|
||||||
|
establishes detachment after copying, not safety during that copy.
|
||||||
|
|
||||||
|
**Potential remediation:** Resolve the return-value ownership contract before
|
||||||
|
v1.0. The straightforward approach is for a successful `Read` to return a
|
||||||
|
per-invocation snapshot that remains stable while PromptKit consumes it, with
|
||||||
|
reuse permitted after the enclosing preparation/execution method returns.
|
||||||
|
Returning fresh owned data is the simplest implementation for concurrent
|
||||||
|
readers. If reuse immediately after `Read` itself returns must remain
|
||||||
|
supported, it requires an explicit borrowing/release arrangement; another
|
||||||
|
unsynchronized copy cannot provide that permission. Add focused coverage for
|
||||||
|
the selected lifetime, without globally serializing unrelated engine work.
|
||||||
|
|
||||||
|
## 2. Profile Inspection Misstates Required-Credential Semantics
|
||||||
|
|
||||||
|
**Priority:** P2. The public inspection contract describes valid results as
|
||||||
|
impossible and incorrectly limits the sources that can satisfy a requirement.
|
||||||
|
|
||||||
|
**Relevant interfaces/code:** [Engine.InspectProfile](../../../engine.go:313),
|
||||||
|
[ProfileInspection.APIKeyRequired](../../../types.go:372),
|
||||||
|
[profile-inspection consumer example](../../consumers/pkg-promptkit.md#inspect-a-profile-before-prompt-work),
|
||||||
|
[effective-target construction](../../../internal/configuration/targets.go:99),
|
||||||
|
and [credential availability check](../../../internal/usecase/runner.go:611).
|
||||||
|
|
||||||
|
**Contract defect:** Both method and field GoDoc claim that `APIKeyRequired`
|
||||||
|
is mutually exclusive with a nonblank `EffectiveModelParams.APIKeyEnv`. They
|
||||||
|
also describe the flag as requiring a direct credential or a request-level
|
||||||
|
environment override. In fact, `api_key_required: true` suppresses a
|
||||||
|
backend-derived environment source only when no explicit profile source was
|
||||||
|
provided. An explicit profile environment source remains usable, as the
|
||||||
|
[credential format reference](../../formats.md#credentials) already states.
|
||||||
|
Inspection can therefore legitimately return both the flag and a nonblank
|
||||||
|
environment name. The consumer example also calls every reported environment
|
||||||
|
source optional, regardless of the flag.
|
||||||
|
|
||||||
|
**Circumstances and consequence:** A profile sets `api_key_required: true` and
|
||||||
|
`api_key_env: PROMPTKIT_INTERFACE_AUDIT_KEY`. Inspection returns `true` and
|
||||||
|
that name; preparation succeeds using its populated environment variable
|
||||||
|
without a direct key or request override. Consumers following the GoDoc may
|
||||||
|
reject this valid combination or report credential readiness incorrectly.
|
||||||
|
|
||||||
|
**Evidence:** A temporary public-API diagnostic demonstrated that combination
|
||||||
|
and successful preparation. The existing
|
||||||
|
[effective-target test](../../../internal/configuration/targets_test.go:88)
|
||||||
|
also includes a required profile with an explicit environment source.
|
||||||
|
|
||||||
|
**Potential remediation:** Describe the flag as a local credential-availability
|
||||||
|
requirement, distinguish source selection from required availability, and
|
||||||
|
remove the mutual-exclusion/direct-only claims. Align the consumer explanation
|
||||||
|
with the format reference and retain inspection's intentional lack of an
|
||||||
|
environment-value lookup. Do not reject the valid configuration or change
|
||||||
|
the public target's stable JSON shape to make the old wording true.
|
||||||
|
|
||||||
|
## 3. The Wire Contract Incorrectly Says Configured Provider Maps Replace
|
||||||
|
|
||||||
|
**Priority:** P2. Conflicting published contracts predict different outbound
|
||||||
|
provider parameters.
|
||||||
|
|
||||||
|
**Relevant interfaces/code:** [integration request-body contract](../../integrations/openai-compatible-chat.md#request-body),
|
||||||
|
[profile defaulting reference](../../formats.md#defaults-and-overrides),
|
||||||
|
[overlayProviderMap](../../../internal/configuration/targets.go:133), and
|
||||||
|
[runtime override merge](../../../internal/usecase/runner.go:562).
|
||||||
|
|
||||||
|
**Contract defect:** The integration document says backend, profile, and request
|
||||||
|
extra-parameter maps all use whole-map replacement. Actual configured profile
|
||||||
|
maps recursively overlay backend defaults, including nested maps. A nonempty
|
||||||
|
runtime `ExecutionTargetOverride.ExtraParams` instead replaces the entire
|
||||||
|
configured map. The format reference and runtime-override GoDoc describe
|
||||||
|
these distinct behaviors correctly.
|
||||||
|
|
||||||
|
**Circumstances and consequence:** A backend supplies `backend_only: true` and
|
||||||
|
`nested: {backend: true}`; its profile supplies `profile_only: true` and
|
||||||
|
`nested: {profile: true}`. All entries survive into the effective target and
|
||||||
|
built-in HTTP body. A reader of the integration contract would expect the
|
||||||
|
backend entries to disappear, potentially leading to surprising provider
|
||||||
|
options or ineffective attempts to clear defaults. A runtime map containing
|
||||||
|
only `request_only: true` does remove the configured entries.
|
||||||
|
|
||||||
|
**Evidence:** A temporary public-API diagnostic observed both forms through an
|
||||||
|
injected client and confirmed the configured merged fields through a controlled
|
||||||
|
built-in HTTP transport. No external request was made. The existing
|
||||||
|
[effective-target test](../../../internal/configuration/targets_test.go:70)
|
||||||
|
also exercises recursive configured overlay.
|
||||||
|
|
||||||
|
**Potential remediation:** Replace the stale configuration-precedence claim
|
||||||
|
with a short link to the format reference. Keep the integration contract
|
||||||
|
focused on flattening the resulting effective map into the HTTP body and on
|
||||||
|
reserved request fields. Preserve the distinct runtime replacement behavior;
|
||||||
|
do not add another merge policy or duplicate its full configuration reference.
|
||||||
|
|
||||||
|
## 4. Identity GoDoc Still Describes Removed Normalization And Version Fields
|
||||||
|
|
||||||
|
**Priority:** P3. Small but concrete public-contract inconsistencies.
|
||||||
|
|
||||||
|
**Relevant interfaces/code:** [ProfileInspection.ProfileID](../../../types.go:364),
|
||||||
|
[ListPrompts GoDoc](../../../listing.go:90), and
|
||||||
|
[literal profile identity test](../../../profile_inspection_contract_test.go:13).
|
||||||
|
|
||||||
|
**Contract defect:** `ProfileInspection.ProfileID` is described as trimmed,
|
||||||
|
but configured IDs and lookups preserve meaningful whitespace exactly.
|
||||||
|
`ListPrompts` promises ownership of each `Version` pointer even though
|
||||||
|
`PromptIdentity` contains only `ID`; collection-level `ReleaseVersion` belongs
|
||||||
|
to the separate collection inventory.
|
||||||
|
|
||||||
|
**Circumstances and consequence:** Inspecting the valid profile `" padded "`
|
||||||
|
returns that exact ID, while looking up `"padded"` fails. Callers expecting the
|
||||||
|
documented trimmed identity can fail later lookups. The nonexistent version
|
||||||
|
field creates an erroneous expectation about available prompt metadata.
|
||||||
|
|
||||||
|
**Evidence:** The existing literal-identity test explicitly protects preserved
|
||||||
|
whitespace and failure of the trimmed lookup; a temporary public-API diagnostic
|
||||||
|
confirmed it. The current `PromptIdentity` declaration and facade conversion
|
||||||
|
have no version field.
|
||||||
|
|
||||||
|
**Potential remediation:** State that the profile ID is the exact selected
|
||||||
|
identity after any namespace qualification, and remove the nonexistent version
|
||||||
|
pointer language. Keep literal identity behavior and collection metadata
|
||||||
|
unchanged; no new normalization, version field, or compatibility guarantee is
|
||||||
|
needed.
|
||||||
|
|
||||||
|
## Existing Findings And Validation
|
||||||
|
|
||||||
|
The earlier error-handling audit already records loss of owning public error
|
||||||
|
categories for nested injected causes, loss of successful-body I/O causes, and
|
||||||
|
pre-dispatch smoke failures incorrectly becoming concluded reports. Those are
|
||||||
|
also interface-contract concerns, not new duplicate findings here. The hidden
|
||||||
|
`ValidationPreparer` requirement behind the runner's `Validator` parameter is
|
||||||
|
already recorded in the [complexity audit](03-complexity.md). No additional
|
||||||
|
independent defect was established in the reviewed prepared-handle lifecycle,
|
||||||
|
optional-value handling, inspection ownership, or observation/result mapping.
|
||||||
|
|
||||||
|
Focused existing tests passed with the local Go 1.27.0 toolchain:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test -race -count=1 -run 'Test(InspectProfile|ListIdentities|ScopedIdentityListings|PublicArtifactReaderAdapter|PublicLLMClientAdapter|ConfigurationInspection|LoadBuildsEffectiveTargetsAfterInheritance)' . ./internal/configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
The separate temporary reader-reuse diagnostic intentionally produced a race
|
||||||
|
report and exited unsuccessfully, substantiating finding 1 despite the green
|
||||||
|
existing tests. Temporary programs and outputs remain outside the repository.
|
||||||
|
All diagnostics were offline with synthetic configuration and credentials;
|
||||||
|
no paid provider calls, new repository tests, or production changes were used.
|
||||||
|
These checks are review evidence, not additional release gates.
|
||||||
249
docs/roadmap/audit/10-equivalence.md
Normal file
249
docs/roadmap/audit/10-equivalence.md
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
# Behavioral Equivalence And Shared Abstraction Audit
|
||||||
|
|
||||||
|
Reviewed 2026-11-04 against commit `5177db0` and the current worktree. Existing
|
||||||
|
audit-document moves were preserved. No production code or repository tests
|
||||||
|
were changed.
|
||||||
|
|
||||||
|
## Assessment And Method
|
||||||
|
|
||||||
|
The public package adapts consumer values into internal configuration and
|
||||||
|
execution components. ConfigKit owns source composition; PromptKit interprets
|
||||||
|
its final handoff and captures indexes and templates. The runner shares
|
||||||
|
preparation and execution machinery across public workflows, generation owns
|
||||||
|
attempts and retries, and capacity owns admission and permits. Domain rules,
|
||||||
|
validation, JSON-value ownership, and observation have existing shared owners.
|
||||||
|
The architecture does not need another public abstraction or a general-purpose
|
||||||
|
utility package to address the opportunities below.
|
||||||
|
|
||||||
|
This review started from tests: direct cross-path comparisons, common test
|
||||||
|
matrices, and separately asserted matching behavior. It then examined the
|
||||||
|
corresponding implementation and callers to distinguish a shared responsibility
|
||||||
|
from similar-looking code. Matching syntax, similar coverage, or duplicated
|
||||||
|
test setup alone did not establish an extraction opportunity.
|
||||||
|
|
||||||
|
Three bounded refactoring opportunities warrant consideration. They provide
|
||||||
|
test-backed scope for findings 1, 4, and 3 respectively in the
|
||||||
|
[complexity audit](03-complexity.md), rather than three additional independent
|
||||||
|
bugs or duplicate remediation projects. None is a newly demonstrated behavioral
|
||||||
|
failure or an automatic release blocker. Existing behavior, not a stronger
|
||||||
|
equivalence promise, is the preservation target.
|
||||||
|
|
||||||
|
## 1. Share The Schema Compilation Lifecycle Across Source And Execution Paths
|
||||||
|
|
||||||
|
### Evidence
|
||||||
|
|
||||||
|
- [TestJSONSchemaNumericConstraintsRetainJSONPrecision](../../../internal/validate/standard_validator_test.go:721)
|
||||||
|
runs the same exact-number constraints through operating-system and `fs.FS`
|
||||||
|
validators using `jsonSchemaValidatorSources`. Both must distinguish adjacent
|
||||||
|
large integers, preserve precise decimal boundaries, and accept the same
|
||||||
|
valid instances.
|
||||||
|
- [TestJSONSchemaDecodingRequiresOneCompleteDocument](../../../internal/validate/standard_validator_test.go:805)
|
||||||
|
uses that same source matrix to require equivalent schema/instance framing
|
||||||
|
and the distinction between operational schema errors and rejected output.
|
||||||
|
- [TestSchemaReferencesPreserveEscapedFilenames](../../../internal/validate/standard_validator_test.go:492)
|
||||||
|
applies the same reference cases to both source implementations, with an
|
||||||
|
explicit platform exception for filenames illegal on Windows.
|
||||||
|
- [TestStandardValidatorPreparedSchemaSurvivesSourceRemoval](../../../internal/validate/standard_validator_test.go:228)
|
||||||
|
and [TestFSValidatorPreparedSchemaSurvivesSourceMutation](../../../internal/validate/standard_validator_test.go:416)
|
||||||
|
separately require the same captured schema to accept an integer-valued
|
||||||
|
object and reject a string-valued one after its source changes. Their source
|
||||||
|
operations differ, but the prepared validation responsibility matches.
|
||||||
|
|
||||||
|
These tests support common schema semantics and compilation ownership; they do
|
||||||
|
not require identical filesystem representations or diagnostic strings.
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
[StandardValidator and FSValidator](../../../internal/validate/standard_validator.go:74)
|
||||||
|
each implement `PrepareValidation` and direct `validateJSONSchema`. The four
|
||||||
|
paths repeat compiler setup, root-resource handling, cancellation boundaries,
|
||||||
|
compilation/error wrapping, and construction or use of executable schema
|
||||||
|
state. The direct host-file path lets its loader fetch the root, whereas the
|
||||||
|
other paths explicitly load/register it. Decoding, compiler construction,
|
||||||
|
schema execution, and content-result classification already have shared helpers;
|
||||||
|
the remaining duplication is the lifecycle around them.
|
||||||
|
|
||||||
|
### Opportunity And Potential Remediation
|
||||||
|
|
||||||
|
Keep source-specific resolution/loading adapters, but introduce one private
|
||||||
|
compilation operation in `internal/validate` that produces the root document
|
||||||
|
and compiled executor for the caller's selected source. Reuse it from prepared
|
||||||
|
validation and from the direct schema-validation callback. Retain the already
|
||||||
|
shared artifact-validation/result logic. This makes future dialect, framing,
|
||||||
|
number-preservation, and cancellation changes less likely to land in only some
|
||||||
|
of the four paths.
|
||||||
|
|
||||||
|
The existing package is the appropriate owner; neither a public compiler API
|
||||||
|
nor a generic filesystem framework is needed. The related runner dependency
|
||||||
|
clarification remains part of complexity-audit finding 1.
|
||||||
|
|
||||||
|
### Tradeoffs And Test Independence
|
||||||
|
|
||||||
|
Direct validation currently rejects malformed output JSON before accessing its
|
||||||
|
schema. Eagerly implementing it as `PrepareValidation` followed by plan
|
||||||
|
validation would change I/O and error ordering. Share compilation when that
|
||||||
|
work is reached, not necessarily the entire entry point. Preserve contained
|
||||||
|
host paths and symlink handling, logical `fs.FS` paths, escaped resource URLs,
|
||||||
|
single-file sources, synchronous cancellation precedence, and operation-local
|
||||||
|
prepared freezing. These source differences belong in their adapters.
|
||||||
|
|
||||||
|
Keep the source matrix and independently specified valid/invalid instances.
|
||||||
|
Do not compute expected results using the new compilation helper or delete
|
||||||
|
source-specific containment/cancellation tests merely because compilation is
|
||||||
|
shared. Provider-facing packaging of external schema references is a separate
|
||||||
|
correctness finding, not solved by this extraction.
|
||||||
|
|
||||||
|
## 2. Centralize Structural Copying Of Already-Normalized JSON Values
|
||||||
|
|
||||||
|
### Evidence
|
||||||
|
|
||||||
|
- [TestPublicLLMClientAdapterGivesClientOwnedNestedValues](../../../llm_adapter_internal_test.go:14)
|
||||||
|
requires nested provider maps, schema values, messages, and cache metadata to
|
||||||
|
remain independent when an injected client mutates and retains its request.
|
||||||
|
- [TestRunnerPreparedExecutionDetachesTransferredSnapshotSources](../../../internal/usecase/prepared_execution_test.go:634)
|
||||||
|
separately requires nested provider maps and schema values to survive source
|
||||||
|
mutation, repeated `Details` calls, and execution without aliasing.
|
||||||
|
- [TestCopyHandlesIndirectionAndIsolatesNestedMutations](../../../internal/jsonvalue/jsonvalue_test.go:156)
|
||||||
|
exercises the underlying JSON-value ownership responsibility with typed
|
||||||
|
collections and repeated acyclic children. The adjacent scalar/collection
|
||||||
|
tests protect compatible concrete types and exact `json.Number` values.
|
||||||
|
- [TestCopyAndCopyMapApplyDistinctEmptyKeyRules](../../../internal/jsonvalue/jsonvalue_test.go:204)
|
||||||
|
explicitly demonstrates a necessary policy distinction: general schema
|
||||||
|
objects can have empty keys, while provider maps cannot.
|
||||||
|
|
||||||
|
The common requirement is an independently owned copy of accepted nested
|
||||||
|
values. These tests do not establish universal equivalence of validation,
|
||||||
|
normalization, and copying on arbitrary Go values.
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
The facade's [copyAny/copyReflectValue](../../../convert.go:406) implements a
|
||||||
|
separate recursive reflection copier for values projected into client requests,
|
||||||
|
prepared details, results, and profile inspections. Internal prepared-state
|
||||||
|
cloning uses [clonePreparedRun](../../../internal/usecase/prepared_execution.go:226)
|
||||||
|
and [jsonvalue.Copy/CopyMap](../../../internal/jsonvalue/jsonvalue.go:30).
|
||||||
|
The latter package already owns compatible JSON-tree copying and validation.
|
||||||
|
|
||||||
|
Ordinary runtime provider values reach facade projection after JSON-value
|
||||||
|
validation/copying. The runner also copies its schema document through
|
||||||
|
[structuredOutputFromValidationPlan](../../../internal/usecase/runner.go:419)
|
||||||
|
before projecting it. Thus these normal runtime paths have a common accepted
|
||||||
|
value domain, even though their entry-point policies are different.
|
||||||
|
|
||||||
|
### Opportunity And Potential Remediation
|
||||||
|
|
||||||
|
Move ownership of trusted JSON-tree cloning into `internal/jsonvalue`, reusing
|
||||||
|
its compatible structural-copy machinery where semantics match, so the facade
|
||||||
|
does not maintain another general reflection traversal. Keep public/domain DTO
|
||||||
|
field projection in the facade. A small internal trusted-value clone operation
|
||||||
|
is a possible approach; reuse of existing fallible operations is another where
|
||||||
|
the caller can preserve the existing error boundary. Select the simpler option
|
||||||
|
after inventorying the concrete callers and values, rather than introducing a
|
||||||
|
configurable reflection framework.
|
||||||
|
|
||||||
|
The meaningful payoff is removing an independent traversal implementation,
|
||||||
|
not wrapping the existing facade copier under another name. This does not
|
||||||
|
require consolidating every pointer or slice helper in the repository.
|
||||||
|
|
||||||
|
### Tradeoffs And Test Independence
|
||||||
|
|
||||||
|
`Copy` validates and bounds values and normalizes null/indirection; the facade
|
||||||
|
copier assumes prior acceptance and can preserve typed nils and pointers.
|
||||||
|
Neither is a drop-in replacement for the other on all possible inputs.
|
||||||
|
Preserve observed types, nil behavior, exact numbers, schema empty keys, and
|
||||||
|
caller ownership at the relevant boundary. An infallible projection must not
|
||||||
|
silently turn a new copy error into a zero value, a panic, or an additional
|
||||||
|
consumer restriction.
|
||||||
|
|
||||||
|
The [configuration-boundary ADR](../../adr/0001-configuration-boundary.md)
|
||||||
|
also remains authoritative. `providerValueForMapping` serves a different,
|
||||||
|
pre-handoff source-encoding purpose and defers unsupported-value errors to
|
||||||
|
ConfigKit. It must not be routed through a rejecting runtime copier as part of
|
||||||
|
this work. Source number conversion is separately recorded in the correctness
|
||||||
|
audit; visual similarity between its reflection switch and a copier is not
|
||||||
|
enough to equate their contracts.
|
||||||
|
|
||||||
|
Keep literal expected values and mutation-based assertions in both facade and
|
||||||
|
internal tests. A shared production copier must not also manufacture their
|
||||||
|
expected snapshots. Retain focused tests for policy differences rather than
|
||||||
|
replacing them with one “all copy functions are equivalent” test.
|
||||||
|
|
||||||
|
## 3. Share If/With Requirement-Branch Orchestration, Not Template Execution
|
||||||
|
|
||||||
|
### Evidence
|
||||||
|
|
||||||
|
- [TestAnalyzeRequirementsClassifiesKnownConditions](../../../internal/prompt/requirements_test.go:268)
|
||||||
|
verifies required/optional classification for selected and skipped `if`
|
||||||
|
branches, and directly compares the recognized branch choice with the real
|
||||||
|
Go renderer. This anchors recognized condition semantics in actual rendering.
|
||||||
|
- [TestAnalyzeRequirementsHandlesWithDotAndNestedReachability](../../../internal/prompt/requirements_test.go:332)
|
||||||
|
requires the same selected/skipped classification for `with` and nested
|
||||||
|
`if`, while separately protecting `with`'s changed dot and preserved root
|
||||||
|
caller-map access.
|
||||||
|
- [TestAnalyzeRequirementsReportsUnresolvedDecisionAndContentConditions](../../../internal/prompt/requirements_test.go:369)
|
||||||
|
protects conditional dependencies and meaningful incompleteness for unknown
|
||||||
|
decisions. It also establishes that an unresolved condition around plain
|
||||||
|
text alone does not make discovery incomplete.
|
||||||
|
- [TestAnalyzeRequirementsDoesNotHideCallerMapScopes](../../../internal/prompt/requirements_test.go:14)
|
||||||
|
compares several scope forms against actual template execution while allowing
|
||||||
|
incomplete analysis for constructs outside recognition scope.
|
||||||
|
|
||||||
|
The tests support one reachability/evidence policy for recognizable branches,
|
||||||
|
with explicit scope differences. They do not require the inspector to execute
|
||||||
|
or completely interpret arbitrary templates.
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
The `IfNode` and `WithNode` cases in
|
||||||
|
[requirementWalker.visit](../../../internal/prompt/requirements.go:451)
|
||||||
|
independently perform inactive-path traversal, condition evaluation, then/else
|
||||||
|
reachability selection, dependency-counter comparison, and limitation recording.
|
||||||
|
`with` adds a different body-dot calculation. `branchReach`, condition
|
||||||
|
evaluation, and `template.IsTrue` already provide shared lower-level behavior,
|
||||||
|
but their surrounding branch protocol is repeated.
|
||||||
|
|
||||||
|
### Opportunity And Potential Remediation
|
||||||
|
|
||||||
|
Extract a small package-private branch traversal operation that owns the common
|
||||||
|
reachability and evidence sequence, with explicit body/else scope handling.
|
||||||
|
Leave node recognition and `withDotIdentity` visible at the node-specific
|
||||||
|
boundary. Keeping this in `internal/prompt` would allow a change to skipped-path
|
||||||
|
handling or incompleteness reporting to be reviewed and implemented once.
|
||||||
|
|
||||||
|
This is the narrowly supported part of complexity-audit finding 3. It does not
|
||||||
|
justify sharing the requirement walker with the real template executor or
|
||||||
|
building an interpreter abstraction.
|
||||||
|
|
||||||
|
### Tradeoffs And Test Independence
|
||||||
|
|
||||||
|
An abstraction that treats `if` and `with` identically would lose caller-map
|
||||||
|
scope information. Preserve `with`'s body-dot behavior, else scope, inactive
|
||||||
|
computed-name handling, and the placement of dependency bookkeeping used to
|
||||||
|
decide whether an unresolved condition matters. A helper with numerous mode
|
||||||
|
flags or callbacks would obscure those differences; prefer a small explicit
|
||||||
|
operation over a generalized AST visitor framework.
|
||||||
|
|
||||||
|
Retain separate node-specific assertions and the independent standard-library
|
||||||
|
rendering comparisons. Extracting production traversal is not a reason to
|
||||||
|
calculate expected inventories using the production branch helper or to
|
||||||
|
strengthen `Complete` into an execution-readiness guarantee.
|
||||||
|
|
||||||
|
## Recommendation And Validation
|
||||||
|
|
||||||
|
Start with schema compilation, which has the clearest shared lifecycle and
|
||||||
|
cross-source test matrix. The branch helper is a smaller local follow-up.
|
||||||
|
Treat JSON-copy consolidation as a more careful ownership refactor: remove a
|
||||||
|
traversal only after its caller/policy differences are accounted for. No new
|
||||||
|
package boundary is necessary solely to carry out these changes, and retaining
|
||||||
|
small source/boundary-specific adapters is appropriate.
|
||||||
|
|
||||||
|
Existing offline tests passed with the local Go 1.27.0 toolchain:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test -race -count=1 ./internal/validate ./internal/prompt ./internal/jsonvalue
|
||||||
|
go test -race -count=1 -run 'Test(PublicLLMClientAdapter|PreparedExecutionFreezesSourcesAndReturnsIndependentDetails|RunnerPreparedExecutionDetachesTransferredSnapshotSources|RunnerRunAndPrepareResolveSameProfileAndEffectiveSettings|RunnerPreparationNormalizesOutputContractConsistently|ValidateRequestMatchesPrepareContract)' . ./internal/usecase
|
||||||
|
```
|
||||||
|
|
||||||
|
Passing tests substantiate the reviewed current behavior, not equivalence of a
|
||||||
|
future refactor. No new tests, production changes, live provider requests,
|
||||||
|
benchmarks, or additional release gates were introduced by this audit.
|
||||||
229
docs/roadmap/audit/11-boundaries.md
Normal file
229
docs/roadmap/audit/11-boundaries.md
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
# Package Cohesion And Architectural Boundary Audit
|
||||||
|
|
||||||
|
Reviewed 2026-11-04 against commit `b39460a` and the current worktree. Existing
|
||||||
|
audit-document moves were preserved. No production code, tests, dependencies,
|
||||||
|
or functionality were changed.
|
||||||
|
|
||||||
|
## Scope, Method, And Architectural Assessment
|
||||||
|
|
||||||
|
Reviewed the 17 repository-owned internal Go packages, their production import
|
||||||
|
relationships, engine assembly, relevant callers, and tests. The public facade
|
||||||
|
was checked where it establishes the boundary around those packages. Third-party
|
||||||
|
package implementations were outside the audit's scope.
|
||||||
|
|
||||||
|
The [current component inventory](../../internal/overview.md) broadly matches
|
||||||
|
the implementation. Configuration owns the final ConfigKit handoff and captured
|
||||||
|
resource indexes; the engine assembles collaborators; use cases coordinate
|
||||||
|
selection, preparation, execution, and diagnostics. Generation owns dispatch
|
||||||
|
and retries, capacity owns admission and permits, and outbound transport,
|
||||||
|
rendering, artifacts, schema validation, observations, and shared values have
|
||||||
|
identifiable owners. No repository-wide package reorganization is warranted.
|
||||||
|
|
||||||
|
The public facade does not leak internal types or receive reverse production
|
||||||
|
imports from internal packages in the checks run below. Deliberate boundaries
|
||||||
|
remain useful: configuration completion differs from request overrides;
|
||||||
|
diagnostic orchestration differs from ordinary execution; domain invariants
|
||||||
|
differ from source-specific presence and error handling. A broad package, a
|
||||||
|
small interface-only package, or an import of another internal component is not
|
||||||
|
itself a cohesion defect.
|
||||||
|
|
||||||
|
Three focused ownership improvements warrant consideration. They are
|
||||||
|
maintainability opportunities, not newly demonstrated runtime bugs or automatic
|
||||||
|
release blockers.
|
||||||
|
|
||||||
|
## 1. Retire The Test-Only Capacity Client As A Production Scheduling Surface
|
||||||
|
|
||||||
|
**Relevant code:** [capacity.NewClient and client.Generate](../../../internal/capacity/client.go:17),
|
||||||
|
[generation constructors and dispatch](../../../internal/generation/coordinator.go:37),
|
||||||
|
[engine assembly](../../../internal/engine/engine.go:157), and
|
||||||
|
[runner scheduling test](../../../internal/usecase/runner_test.go:2391).
|
||||||
|
|
||||||
|
**Basis:** The engine selects `generation.NewBuiltIn` or `NewInjected` for every
|
||||||
|
production engine. The coordinator owns active-permit acquisition around each
|
||||||
|
attempt, retry waits, observation, and diagnostic evidence. The earlier
|
||||||
|
`capacity.NewClient` still provides a separate model-client wrapper that
|
||||||
|
acquires/releases a permit around an opaque call. Both use the same manager;
|
||||||
|
there are not two independent FIFO algorithms or pools.
|
||||||
|
|
||||||
|
Caller analysis found only tests invoking the older constructor: capacity tests
|
||||||
|
and a runner test that assembles initial/repair scheduling with this wrapper.
|
||||||
|
The [capacity document](../../internal/capacity.md#fifo-generation-permits)
|
||||||
|
acknowledges its retention for focused callers/tests, but no production caller
|
||||||
|
was found. Its presence leaves capacity owning a model-client adapter after
|
||||||
|
production dispatch responsibility moved to generation, and makes one runner
|
||||||
|
assembly test exercise a different scheduling composition than the engine.
|
||||||
|
|
||||||
|
**Consequences:** The extra adapter and its documented role remain maintenance
|
||||||
|
surface without a production need. Future scheduling changes can require
|
||||||
|
reasoning about a legacy assembly alongside the real one. Green tests of the
|
||||||
|
wrapper alone do not demonstrate that coordinator dispatch applies the same
|
||||||
|
permit lifetime. Existing coordinator and facade tests do protect that actual
|
||||||
|
path, so this is not a claim that production scheduling is untested or broken.
|
||||||
|
|
||||||
|
**Potential remediation:** Keep the manager and its admission/FIFO primitives
|
||||||
|
in `internal/capacity`, and generation dispatch integration in
|
||||||
|
`internal/generation`. Replace the runner test's wrapper with the production
|
||||||
|
injected coordinator. For focused capacity scenarios, test `Manager.Acquire`
|
||||||
|
directly or retain the small one-shot adapter as test-owned scaffolding rather
|
||||||
|
than a production constructor. Once callers are adjusted, the production
|
||||||
|
wrapper can be retired and its documentation updated.
|
||||||
|
|
||||||
|
Preserve the useful FIFO, cancellation-race, independent-pool, and panic-release
|
||||||
|
assertions when adjusting test ownership. This finding does not call for moving
|
||||||
|
queue internals into generation, deleting those scenarios, or wrapping the
|
||||||
|
coordinator in another capacity client, which could acquire the same limit
|
||||||
|
twice. Retaining the wrapper is viable if a real internal production need is
|
||||||
|
identified; current test reachability alone does not establish one.
|
||||||
|
|
||||||
|
## 2. Put OpenRouter Authentication Protocol Details Behind The Outbound Adapter
|
||||||
|
|
||||||
|
**Relevant code:** [authenticationProbe.runOpenRouterKey](../../../internal/usecase/smoke_authentication.go:66),
|
||||||
|
[recognizedOpenRouterKeyResponse](../../../internal/usecase/smoke_authentication.go:139),
|
||||||
|
[diagnostic endpoint derivation](../../../internal/usecase/smoke_selection.go:166),
|
||||||
|
and [HTTP authentication transport](../../../internal/llm/http_evidence.go:111).
|
||||||
|
|
||||||
|
**Basis:** `internal/usecase` currently interprets OpenRouter-specific HTTP
|
||||||
|
statuses and the account-response keys `data`, `label`, `is_free_tier`, and
|
||||||
|
`usage`. It owns JSON envelope parsing, identifying-field type checks, and body
|
||||||
|
consumption. The same package derives the OpenRouter-relative `/key` endpoint.
|
||||||
|
Meanwhile `internal/llm` owns authenticated HTTP execution, credential
|
||||||
|
resolution, evidence collection, and response ownership mechanics. One provider
|
||||||
|
integration is therefore divided between the application coordinator and its
|
||||||
|
outbound adapter.
|
||||||
|
|
||||||
|
This division is different from a use case consuming factual HTTP evidence:
|
||||||
|
the precise external account document and provider endpoint are protocol
|
||||||
|
knowledge. The [architecture policy](../../policy/architecture.md#architectural-approach)
|
||||||
|
calls for encapsulating external integration details in internal adapters.
|
||||||
|
The parser is already functionally isolated, making the ownership seam fairly
|
||||||
|
small. The authentication tests in
|
||||||
|
[smoke_authentication_test.go](../../../internal/usecase/smoke_authentication_test.go:64)
|
||||||
|
also show that this is provider protocol behavior rather than application
|
||||||
|
selection policy.
|
||||||
|
|
||||||
|
**Consequences:** A change to OpenRouter's account endpoint or identifying
|
||||||
|
response fields requires changing application-layer code as well as the
|
||||||
|
transport integration. Adding another authentication adapter would extend
|
||||||
|
provider-specific branching inside the use-case package. This complicates the
|
||||||
|
otherwise clear distinction between choosing a diagnostic and implementing
|
||||||
|
the upstream protocol.
|
||||||
|
|
||||||
|
**Potential remediation:** Move the OpenRouter request/response adapter,
|
||||||
|
including its default destination and response recognition, into
|
||||||
|
`internal/llm` alongside the existing diagnostic transport. Have the use case
|
||||||
|
consume a small factual outcome/evidence value. Keep target selection,
|
||||||
|
configured applicability, unsupported-operation reporting, caller cancellation,
|
||||||
|
overall completion, and report assembly in the use case. The existing
|
||||||
|
consumer-owned diagnostic interface is a useful seam; engine assembly can
|
||||||
|
supply the concrete outbound implementation without requiring protocol parsing
|
||||||
|
inside the coordinator.
|
||||||
|
|
||||||
|
An alternative is a focused internal diagnostic-adapter package if separation
|
||||||
|
from inference transport becomes useful. Another package or a provider plugin
|
||||||
|
framework is not necessary for the current single protocol adapter. Do not
|
||||||
|
introduce a new dependency from the outbound adapter back into `usecase`, or
|
||||||
|
move application report/completion policy into transport merely to relocate
|
||||||
|
the parser. The diagnostic feature legitimately deals with HTTP facts; a
|
||||||
|
blanket prohibition on HTTP types or status observations in use cases is not
|
||||||
|
implied by this finding.
|
||||||
|
|
||||||
|
Preserve anonymous authentication probing, explicit endpoint overrides,
|
||||||
|
neutral tri-state facts, private body/credential handling, and the absence of
|
||||||
|
ordinary observer events. The previously recorded unbounded authentication-body
|
||||||
|
read is a separate resource/security finding; relocation alone does not fix it.
|
||||||
|
|
||||||
|
## 3. Narrow Smoke-Value Copy Helpers To Their Actual Ownership Boundaries
|
||||||
|
|
||||||
|
**Relevant code:** [domain.CloneSmokeTestRequest](../../../internal/domain/smoke_diagnostics.go:157),
|
||||||
|
[domain.CloneSmokeTestReport](../../../internal/domain/smoke_diagnostics.go:179),
|
||||||
|
[smokeReportTarget](../../../internal/usecase/smoke_tester.go:432), and
|
||||||
|
[public smoke-value conversion](../../../smoke_diagnostics.go:279).
|
||||||
|
|
||||||
|
**Basis:** `CloneSmokeTestRequest` is called only by its own domain test;
|
||||||
|
actual request ownership is established by public-to-domain conversion.
|
||||||
|
`CloneSmokeTestReport` has one production caller: `smokeReportTarget` creates
|
||||||
|
an otherwise empty report containing a target, clones the full report, and
|
||||||
|
extracts just that target. Whole-report facts, timing, and attempt cloning have
|
||||||
|
no runtime use through that caller. Public report conversion independently
|
||||||
|
copies the actual facts, timings, attempts, and provider map into public types.
|
||||||
|
|
||||||
|
Thus the domain package maintains a broad request/report-copy API, but the
|
||||||
|
real internal consumer needs only a detached target/provider map. The
|
||||||
|
whole-report ownership functionality is exercised by its domain test rather
|
||||||
|
than by actual whole-report production use. Public DTO conversion is a
|
||||||
|
legitimate separate responsibility, not something that should disappear into
|
||||||
|
domain types or public aliases.
|
||||||
|
|
||||||
|
**Consequences:** Smoke-value changes have additional copy lists and tests to
|
||||||
|
maintain even where those copies are not needed in production. A temporary
|
||||||
|
report container obscures the narrow target-detachment operation. Keeping a
|
||||||
|
general cloner solely because its own test calls it reverses the relationship
|
||||||
|
between production responsibility and supporting tests.
|
||||||
|
|
||||||
|
**Potential remediation:** Express the target-copy operation directly at its
|
||||||
|
use-case boundary using existing JSON-value ownership, or a small target clone
|
||||||
|
helper if actual callers justify one. Retain public request/report projection
|
||||||
|
in the facade and shared JSON-tree copying in its existing owner. Retire the
|
||||||
|
unused request cloner and the unnecessary whole-report clone surface after
|
||||||
|
confirming no additional callers need them. Remove only clone-specific helper
|
||||||
|
code that is then unused. The backend-cloning helpers have real registry
|
||||||
|
consumers and are outside the proposed retirement; retain that metadata
|
||||||
|
ownership behavior.
|
||||||
|
|
||||||
|
Preserve provider-map error propagation, caller-owned result values, and
|
||||||
|
credential/endpoint redaction. Keep the meaningful
|
||||||
|
[public conversion ownership test](../../../smoke_conversion_internal_test.go:13)
|
||||||
|
and relevant use-case report tests. If genuine internal whole-report snapshots
|
||||||
|
are needed, retaining an explicitly used domain cloner is a reasonable
|
||||||
|
alternative. This finding does not require sharing public/internal DTO types
|
||||||
|
or routing public conversion through a full internal clone followed by another
|
||||||
|
copy. Coordinate any JSON traversal consolidation with
|
||||||
|
[audit 10](10-equivalence.md#2-centralize-structural-copying-of-already-normalized-json-values).
|
||||||
|
|
||||||
|
## Related Existing Findings
|
||||||
|
|
||||||
|
The runner's declared `Validator` dependency and actual `ValidationPreparer`
|
||||||
|
requirement remain the interface-boundary clarification in
|
||||||
|
[complexity-audit finding 1](03-complexity.md#1-consolidate-schema-compilation-and-express-the-runners-actual-interface).
|
||||||
|
The facade's generic JSON reflection copier has an existing internal owner
|
||||||
|
identified in audit 10. These are relevant ownership concerns, not additional
|
||||||
|
independent findings to count again. Large orchestration functions and repeated
|
||||||
|
schema compilation within an already coherent package likewise retain their
|
||||||
|
existing complexity/equivalence remediation scope.
|
||||||
|
|
||||||
|
## Reachability And Validation Evidence
|
||||||
|
|
||||||
|
Production imports were enumerated with `go list` and compared with the graph
|
||||||
|
and source callers. Rapid Type Analysis used the locally available pinned
|
||||||
|
`golang.org/x/tools` v0.52.0 command, with reporting restricted to
|
||||||
|
repository-owned internal packages:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go list -f '{{.ImportPath}}|{{join .Imports " "}}' ./internal/...
|
||||||
|
go run golang.org/x/tools/cmd/deadcode@v0.52.0 -test -filter='^go.maximumdirect.net/promptkit/internal/' ./...
|
||||||
|
go run golang.org/x/tools/cmd/deadcode@v0.52.0 -filter='^go.maximumdirect.net/promptkit/internal/' ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
With tests included, the tool reported only the unused test helper
|
||||||
|
`internal/usecase/runner_test.go:stringPtr`. That small test cleanup does not
|
||||||
|
justify a separate architectural finding. Test-only production helpers remain
|
||||||
|
reachable to this analysis when their tests call them.
|
||||||
|
|
||||||
|
Without tests, the roots are the repository's example executables, not every
|
||||||
|
possible downstream library consumer. The output includes valid public
|
||||||
|
workflows that those examples do not exercise, so it is not a deletion list.
|
||||||
|
The capacity-wrapper and request-cloner observations above were additionally
|
||||||
|
checked against actual source references and assembly; graph in-degree or
|
||||||
|
example reachability alone was not treated as proof of obsolescence. The
|
||||||
|
analysis also covers only the selected local build configuration.
|
||||||
|
|
||||||
|
The following existing offline checks passed with local Go 1.27.0:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test -race -count=1 -run 'Test(PublicFacadeDoesNotExposeInternalTypes|InternalProductionPackagesDoNotImportFacade|RepositoryDoesNotImportFormerModule|SmokeTestConcludedInferenceMismatchHasNilGoErrorAndDetachedValues)' .
|
||||||
|
go test -race -count=1 ./internal/capacity ./internal/generation ./internal/domain ./internal/usecase
|
||||||
|
```
|
||||||
|
|
||||||
|
No new release gate, consumer contract, provider call, production edit, or test
|
||||||
|
removal is proposed as part of this audit pass. The tools left module metadata
|
||||||
|
unchanged.
|
||||||
233
docs/roadmap/audit/12-deadcode.md
Normal file
233
docs/roadmap/audit/12-deadcode.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# Dead Code And Obsolete Functionality Audit
|
||||||
|
|
||||||
|
Reviewed 2026-11-04 against commit `3b76031` and the current worktree. Existing
|
||||||
|
audit-document moves were preserved. No production code, tests, dependencies,
|
||||||
|
or functionality were changed.
|
||||||
|
|
||||||
|
## Scope And Assessment
|
||||||
|
|
||||||
|
Reviewed repository-owned Go packages, public entry points, configuration and
|
||||||
|
runtime values, collaborator interfaces, default/optional paths, embedded
|
||||||
|
resources, build constraints, and the release launchers and their wiring test.
|
||||||
|
The architecture remains a public facade over captured configuration, use-case
|
||||||
|
orchestration, generation/capacity, and focused internal adapters. The former
|
||||||
|
standalone configuration repositories have already been retired; this review
|
||||||
|
found smaller residues rather than another abandoned subsystem.
|
||||||
|
|
||||||
|
The review combined Rapid Type Analysis, Staticcheck unused/data-flow checks,
|
||||||
|
graph caller discovery, source inspection, and the format, internal, policy,
|
||||||
|
and roadmap documents. Tests count as real callers, but a helper's self-test
|
||||||
|
does not by itself establish a production need. Conversely, absence from the
|
||||||
|
examples or default execution path is not evidence that an optional public
|
||||||
|
feature is obsolete.
|
||||||
|
|
||||||
|
Four cleanup areas warrant consideration. These are low-priority reductions in
|
||||||
|
state and maintenance surface, not newly demonstrated runtime bugs. None calls
|
||||||
|
for deleting a supported public feature or imposing a new release gate.
|
||||||
|
|
||||||
|
## 1. Remove Backend Presence Bookkeeping With No Runtime Consumer
|
||||||
|
|
||||||
|
**Relevant code:** [loaded.backends](../../../internal/configuration/load.go:122),
|
||||||
|
[backendFields and presence capture](../../../internal/configuration/load.go:215),
|
||||||
|
[effective backend construction](../../../internal/configuration/targets.go:14),
|
||||||
|
and [Backend.QueueCapacitySet](../../../internal/domain/domain.go:187).
|
||||||
|
|
||||||
|
**Evidence:** Construction allocates a per-backend field-state map and fills it
|
||||||
|
through `captureFields(..., state.backends)`. No production code reads the
|
||||||
|
recorded backend states. The only lookup of the captured queue state is in
|
||||||
|
[TestLoadDefaultsAbsentQueueCapacity](../../../internal/configuration/load_test.go:755).
|
||||||
|
Unlike profile presence, backend presence is not consumed while constructing
|
||||||
|
effective execution targets.
|
||||||
|
|
||||||
|
`QueueCapacitySet` is also assigned during target construction and asserted in
|
||||||
|
tests, but never read to select a production behavior. The current assignment
|
||||||
|
is simply whether concurrency is positive; it no longer distinguishes an
|
||||||
|
explicit queue setting from a derived default. The registry's capacity-policy
|
||||||
|
projection uses `ConcurrencyLimit` and `QueueCapacity`, not this flag.
|
||||||
|
|
||||||
|
**Why obsolete:** Queue defaulting and its provenance are already handled by
|
||||||
|
[resolveBackends](../../../internal/configuration/resolve_targets.go:11) and
|
||||||
|
ConfigKit's derived-default record. Typed final values drive runtime capacity;
|
||||||
|
the retained side map and Boolean do not contribute to that behavior or to the
|
||||||
|
public inspection view. No documented compatibility or planned-feature need for
|
||||||
|
these extra records was found.
|
||||||
|
|
||||||
|
**Potential remediation:** Remove the backend-only presence capture, its field
|
||||||
|
list and retained map, and the unused runtime Boolean/assignment. Adjust tests
|
||||||
|
to assert effective queue behavior and, where relevant, the existing ConfigKit
|
||||||
|
inspection/provenance result. Keep `captureFields`, profile field-state
|
||||||
|
tracking, and prompt-input metadata presence: those have actual consumers and
|
||||||
|
preserve required absent/null/explicit-value distinctions. Retain explicit
|
||||||
|
zero queue capacity, derived defaulting, and operator history unchanged.
|
||||||
|
|
||||||
|
This removes unnecessary construction work and retained per-backend maps; it
|
||||||
|
does not justify dropping configuration provenance or changing its ownership
|
||||||
|
under the [configuration-boundary ADR](../../adr/0001-configuration-boundary.md).
|
||||||
|
|
||||||
|
## 2. Retire Legacy Fields In The Captured Prompt Representation
|
||||||
|
|
||||||
|
**Relevant code:** [PromptDefinition.OutputFormat](../../../internal/domain/domain.go:148),
|
||||||
|
[PromptMessageTemplate.ContentFile](../../../internal/domain/domain.go:173),
|
||||||
|
[prompt materialization](../../../internal/configuration/prompts.go:58),
|
||||||
|
[resolveOutputContract](../../../internal/usecase/runner.go:639), and
|
||||||
|
[definition hashing](../../../internal/promptdef/hash.go:12).
|
||||||
|
|
||||||
|
**Evidence:** Supported engine construction requires and validates a complete
|
||||||
|
output contract. Materialization sets `definition.Validation` to that contract
|
||||||
|
and duplicates its format into `definition.OutputFormat`. The runner reads
|
||||||
|
the duplicate only when `Validation.Format` is empty, a condition that the
|
||||||
|
captured definitions supplied by engine construction cannot have. The standard
|
||||||
|
runner test-definition helper fills both fields with the same value; no
|
||||||
|
supported public repository-injection path supplies an alternative definition.
|
||||||
|
|
||||||
|
For messages, materialization resolves the real source `content_file` into
|
||||||
|
captured `Content` and does not populate the domain `ContentFile` field. The
|
||||||
|
renderer uses `Content`/captured templates, not that path field. Its one
|
||||||
|
nonempty assignment is a renderer fixture demonstrating that already-loaded
|
||||||
|
content is rendered without opening a file. The source-format
|
||||||
|
`promptMessageConfig.ContentFile` remains actively used and is a different
|
||||||
|
field.
|
||||||
|
|
||||||
|
**Why obsolete:** The runtime representation still carries source-era message
|
||||||
|
path data and a second output-format source after configuration loading became
|
||||||
|
the authoritative materialization boundary. The empty-format fallback is
|
||||||
|
disconnected from supported engine definitions. No current documentation or
|
||||||
|
roadmap records a compatibility requirement for these internal duplicate fields.
|
||||||
|
|
||||||
|
**Potential remediation:** Use `PromptDefinition.Validation.Format` as the one
|
||||||
|
captured format and remove the legacy duplicate/fallback. Remove only the
|
||||||
|
runtime message path field, adjusting its fixture to exercise captured text.
|
||||||
|
Preserve the actual YAML `content_file` feature, source-relative resource
|
||||||
|
resolution, ConfigKit origin inspection, and construction-time capture. Also
|
||||||
|
preserve the separate, supported request-override rule that an empty replacement
|
||||||
|
`OutputContract.Format` defaults to text.
|
||||||
|
|
||||||
|
**Serialization caveat:** These fields are included indirectly by
|
||||||
|
`HashDefinition`'s `json.Marshal`, even when a message path is empty. They are
|
||||||
|
therefore not completely inert bytes: removing them changes current prompt
|
||||||
|
hash values. Account for that effect explicitly and protect the documented
|
||||||
|
opaque-equality relationships between inspection and preparation. Do not claim
|
||||||
|
bit-identical hashes after changing the hash input, or invent a permanent hash
|
||||||
|
encoding guarantee merely to perform this cleanup. The fields are internal
|
||||||
|
representation, not part of the public stable DTO shapes.
|
||||||
|
|
||||||
|
## 3. Remove Unused Diagnostic State And A Discarded Helper Result
|
||||||
|
|
||||||
|
**Relevant code:** [reachabilityProbeOutcome.initialEndpoint](../../../internal/usecase/smoke_reachability.go:24),
|
||||||
|
[HTTP report assembly](../../../internal/usecase/smoke_tester.go:376),
|
||||||
|
[fileSource](../../../internal/engine/engine.go:211), and
|
||||||
|
[SetSchemaFile](../../../internal/engine/engine.go:75).
|
||||||
|
|
||||||
|
**Evidence:** The reachability probe assigns its private outcome's
|
||||||
|
`initialEndpoint`, but the smoke tester never reads that member. It records
|
||||||
|
the real public attempt's `InitialEndpoint` from the resolved report target.
|
||||||
|
Only `TestReachabilityProbeRemovesURLCredentials` inspects the private echo.
|
||||||
|
Actual sanitized request/evidence and report behavior are established through
|
||||||
|
other values.
|
||||||
|
|
||||||
|
Separately, `fileSource` returns a filesystem, file name, source-directory
|
||||||
|
string, and error. Its only caller, `SetSchemaFile`, always discards the
|
||||||
|
directory result. The directory is still needed internally to construct
|
||||||
|
`os.DirFS`; exposing it as another return value serves no caller.
|
||||||
|
|
||||||
|
**Potential remediation:** Remove the unused probe-outcome member and its
|
||||||
|
assignment; retain credential-removal assertions against the actual request,
|
||||||
|
evidence, or public diagnostic report. Reduce `fileSource`'s private signature
|
||||||
|
to the consumed results. Keep its directory calculation, file checks, selected
|
||||||
|
filesystem/root, and the public `WithSchemaFile` option intact.
|
||||||
|
|
||||||
|
Neither cleanup removes `SmokeTestAttempt.InitialEndpoint`, destination
|
||||||
|
evidence, or single-schema-file support. Those public features are live even
|
||||||
|
though an extra private field/result is unnecessary.
|
||||||
|
|
||||||
|
## 4. Remove Orphan Helpers Whose Only Callers Are Their Own Tests
|
||||||
|
|
||||||
|
**Relevant code and evidence:**
|
||||||
|
|
||||||
|
- [filecatalog.RelativePath](../../../internal/filecatalog/catalog.go:71) is
|
||||||
|
called only by `TestRelativePathNested`. Current source discovery and
|
||||||
|
source-relative resource resolution use other helpers; no production or
|
||||||
|
public-facade caller uses this wrapper around `filepath.Rel`.
|
||||||
|
- [resourceNamespaceFromID](../../../internal/domain/resource_identity.go:34)
|
||||||
|
is called only by `TestResourceNamespaceFromID`. Actual namespace inventory
|
||||||
|
and filtering paths do not call it. The exported identity-validation and
|
||||||
|
qualification operations remain active and are not candidates for removal.
|
||||||
|
- [runner_test.go:stringPtr](../../../internal/usecase/runner_test.go:2592)
|
||||||
|
has no callers, including in tests. Both reachability analysis and
|
||||||
|
Staticcheck `U1000` report it.
|
||||||
|
|
||||||
|
Caller searches found no reflection, registration, build-constrained caller,
|
||||||
|
or supported external API exposing these helpers. The first two belong to
|
||||||
|
internal packages and have only their direct self-tests; no documented future
|
||||||
|
or compatibility purpose was found. The third is already test-only and unused.
|
||||||
|
|
||||||
|
**Potential remediation:** Remove the orphan helpers and their now-irrelevant
|
||||||
|
self-tests, keeping behavioral coverage of real discovery, namespace
|
||||||
|
inventory/filtering, and identity qualification. Remove the unused pointer
|
||||||
|
helper without replacing it. There is no reason to introduce a new abstraction
|
||||||
|
or move the self-tests to another layer solely to preserve these unused APIs.
|
||||||
|
|
||||||
|
## Intentional Retention And Related Findings
|
||||||
|
|
||||||
|
The following distinctions prevent removal based on misleading reachability:
|
||||||
|
|
||||||
|
- `WithSchemaFile` and `InlineWithURI` are supported public helpers despite
|
||||||
|
being unreachable from the current test executables. The former's missing
|
||||||
|
public wiring coverage is already in [audit 08](08-testing.md); it is not
|
||||||
|
evidence that the option should be deleted.
|
||||||
|
- The older capacity client wrapper and no-repair runner constructor are
|
||||||
|
expressly retained for focused internal callers/tests in the current
|
||||||
|
[capacity](../../internal/capacity.md#fifo-generation-permits) and
|
||||||
|
[runner](../../internal/runner.md#collaborators) documentation. This audit
|
||||||
|
does not classify them as dead functionality or recommend deleting them.
|
||||||
|
[Audit 11](11-boundaries.md) discusses the wrapper's architectural placement;
|
||||||
|
altering that deliberate retention is a separate decision.
|
||||||
|
- Repository capability fallbacks, standalone rendering/validation, injected
|
||||||
|
clients' nil usage-presence compatibility, nil-validator/admitter behavior,
|
||||||
|
and optional provider settings have meaningful callers or documented
|
||||||
|
purposes. Normal engine assembly taking another path does not obsolete them.
|
||||||
|
- ConfigKit YAML fields and stable JSON DTO fields can be used by reflection;
|
||||||
|
formatter methods and interfaces can be invoked indirectly. Embedded
|
||||||
|
OpenRouter and smoke-schema assets, the Linux FIFO test, and release scripts
|
||||||
|
likewise have consumers outside a simple direct-call list. They were not
|
||||||
|
treated as dead. No unused public configuration option or abandoned release
|
||||||
|
launcher was established.
|
||||||
|
|
||||||
|
The domain smoke-request/full-report cloners have a narrow-or-unused consumer
|
||||||
|
surface already described in [audit 11, finding 3](11-boundaries.md#3-narrow-smoke-value-copy-helpers-to-their-actual-ownership-boundaries).
|
||||||
|
That ownership/remediation scope is not counted again here. The schema and
|
||||||
|
JSON-copy consolidations in prior audits likewise remain their existing work,
|
||||||
|
not an excuse to delete intentional alternative implementations wholesale.
|
||||||
|
|
||||||
|
## Tooling And Validation
|
||||||
|
|
||||||
|
The checks used local Go 1.27.0 and pinned audit-tool versions:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run golang.org/x/tools/cmd/deadcode@v0.52.0 -test -filter='^go.maximumdirect.net/promptkit(/|$)' ./...
|
||||||
|
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -checks 'U1000,SA4*' ./...
|
||||||
|
go list -f '{{.ImportPath}} {{join .GoFiles " "}} {{join .IgnoredGoFiles " "}}' ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
`deadcode -test` reported the two public helpers and unused test pointer helper
|
||||||
|
listed above. Staticcheck reported only `stringPtr` (`U1000`) and exited with
|
||||||
|
status 1 because of that finding; it reported no additional selected `SA4*`
|
||||||
|
diagnostics. These are audit observations, not successful lint sign-offs.
|
||||||
|
Neither tool establishes that all tested or serialized state is necessary,
|
||||||
|
which is why the write-only and redundant fields required manual review.
|
||||||
|
|
||||||
|
Rapid Type Analysis starts from executable roots; tests add roots, and example
|
||||||
|
programs do not exercise every library consumer path. The analysis is local-build
|
||||||
|
evidence, not proof about all platforms or downstream programs. In particular,
|
||||||
|
platform-specific schema handling and optional library operations were retained.
|
||||||
|
|
||||||
|
Existing offline race-enabled tests passed:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test -race -count=1 ./internal/configuration ./internal/domain ./internal/filecatalog ./internal/prompt ./internal/engine ./internal/usecase
|
||||||
|
go test -race -count=1 -run 'Test(ConfigurationInspection|PreparedExecution|ValidateRequestMatchesPrepareContract|AppendedMessagesComposeFrozenEffectivePrompt)' .
|
||||||
|
```
|
||||||
|
|
||||||
|
Tool execution left `go.mod` and `go.sum` unchanged. No production changes,
|
||||||
|
test removals, provider calls, or new validation gates were made during this
|
||||||
|
review.
|
||||||
570
docs/roadmap/audit/catalog.md
Normal file
570
docs/roadmap/audit/catalog.md
Normal file
@@ -0,0 +1,570 @@
|
|||||||
|
# Consolidated Pre-1.0 Audit Catalog
|
||||||
|
|
||||||
|
Consolidated on 2026-11-04 from the 11 audit documents in this directory.
|
||||||
|
Their 41 numbered findings are represented by 34 entries below. Entries that
|
||||||
|
describe the same underlying issue share one remediation scope; missing
|
||||||
|
regression coverage for an identified defect is included with that defect.
|
||||||
|
Some broad testing findings consequently support more than one entry.
|
||||||
|
|
||||||
|
The order reflects consequence, exposure, and the value of clarifying public
|
||||||
|
contracts before v1.0. Confirmed defects and conditional security exposure come
|
||||||
|
first, followed by contract/test gaps, resource efficiency, and maintainability.
|
||||||
|
This ordering does not upgrade the original severities or turn every cleanup
|
||||||
|
into a release gate. The source audits retain their evidence, qualifications,
|
||||||
|
and validation results. None of these entries is marked remediated by this
|
||||||
|
documentation-only consolidation.
|
||||||
|
|
||||||
|
## 1. Update The Audited Maintainer Toolchain For Applicable Security Fixes
|
||||||
|
|
||||||
|
**Conditional security exposure.** The audited Go 1.27.0 toolchain predates
|
||||||
|
networking fixes applicable to HTTP clients, including HTTP/2 CPU exhaustion
|
||||||
|
and conditional certificate-trust exposure. Use a patched maintainer toolchain
|
||||||
|
and repeat the focused security assessment against it. Actual downstream
|
||||||
|
exposure depends on the toolchain that builds the consumer and its transport
|
||||||
|
and trust configuration; raising PromptKit's minimum Go requirement is a
|
||||||
|
separate compatibility decision, not an implied remedy.
|
||||||
|
|
||||||
|
Sources: [Security 1](07-security.md#1-the-audited-go-toolchain-predates-applicable-networking-security-fixes).
|
||||||
|
|
||||||
|
## 2. Bound Authentication-Probe Response Consumption
|
||||||
|
|
||||||
|
**Confirmed resource/security defect.** Authentication diagnostics read a
|
||||||
|
successful upstream body with unbounded `io.ReadAll`. A deadline does not bound
|
||||||
|
the bytes a fast server can supply. Introduce finite response consumption and
|
||||||
|
clean closure, with an explicit choice of byte budget and overflow reporting.
|
||||||
|
Overflow should not be interpreted as proof of rejected authentication.
|
||||||
|
Relocating the provider adapter in entry 26 would not itself fix this defect.
|
||||||
|
|
||||||
|
Sources: [Resources 1](05-resources.md#1-authentication-diagnostics-read-an-unbounded-upstream-body),
|
||||||
|
[Security 2](07-security.md#2-authentication-probes-permit-unbounded-remote-body-memory-consumption).
|
||||||
|
|
||||||
|
## 3. Avoid Exponential Expansion During Convenience Source Conversion
|
||||||
|
|
||||||
|
**Confirmed resource defect.** `ProfileSource` and `BackendSource` conversion
|
||||||
|
expand repeated references before ConfigKit performs its bounded snapshot.
|
||||||
|
A small shared acyclic graph can therefore cause exponential work and
|
||||||
|
allocation. Preserve shared references during conversion, with suitable cycle
|
||||||
|
and slice-view handling, rather than duplicating ConfigKit's validation or
|
||||||
|
introducing PromptKit-side raw-source limits. Add focused shared-graph and
|
||||||
|
deferred-failure regressions for both convenience helpers.
|
||||||
|
|
||||||
|
Sources: [Resources 2](05-resources.md#2-convenience-source-conversion-expands-shared-graphs-before-bounded-loading),
|
||||||
|
[Testing 3](08-testing.md#3-programmatic-provider-value-conversion-lacks-representative-value-cases).
|
||||||
|
|
||||||
|
## 4. Resolve The ArtifactReader Buffer-Ownership Contract
|
||||||
|
|
||||||
|
**Confirmed race under the documented collaborator contract.** The public
|
||||||
|
reader contract permits buffer reuse after `Read` returns, before PromptKit
|
||||||
|
can finish copying it. Concurrent preparation with a compliant custom reader
|
||||||
|
can race. Clarify ownership/lifetime per invocation and align adapters, GoDoc,
|
||||||
|
and concurrency tests. This needs a deliberate public-contract decision before
|
||||||
|
v1.0: an immediate copy alone cannot guarantee safety if the reader is allowed
|
||||||
|
to mutate the buffer immediately. No default-reader defect was established.
|
||||||
|
|
||||||
|
Sources: [Interfaces 1](09-interfaces.md#1-artifactreader-permits-buffer-reuse-before-promptkit-can-copy-it).
|
||||||
|
|
||||||
|
## 5. Preserve Exact Numbers In Programmatic Provider Sources
|
||||||
|
|
||||||
|
**Confirmed data-integrity defect.** Convenience source conversion falls back
|
||||||
|
from integer parsing to `float64` without checking precision, silently changing
|
||||||
|
some accepted `json.Number` values. Preserve an exact ConfigKit-compatible
|
||||||
|
representation, or defer an unsupported-value error to ConfigKit instead of
|
||||||
|
rounding or converting the number into a string. Regression coverage should
|
||||||
|
include exact-number boundaries, nested typed collections, mutation isolation,
|
||||||
|
and deferred unsupported/cyclic-value errors for both source helpers.
|
||||||
|
|
||||||
|
Sources: [Correctness 1](01-correctness.md#1-provider-source-helpers-silently-change-exact-json-numbers),
|
||||||
|
[Testing 3](08-testing.md#3-programmatic-provider-value-conversion-lacks-representative-value-cases).
|
||||||
|
|
||||||
|
## 6. Make Provider-Native Schema Constraints Self-Contained
|
||||||
|
|
||||||
|
**Confirmed implementation gap; packaging policy needs selection.** Local
|
||||||
|
validation freezes referenced schema resources, but the provider receives only
|
||||||
|
the root document and cannot resolve its local external references. Select
|
||||||
|
correct resource packaging, or an explicit local-only fallback policy, without
|
||||||
|
naively inlining recursive schemas or silently dropping native constraints.
|
||||||
|
Add a wire-level regression demonstrating a usable provider constraint, not
|
||||||
|
merely successful local compilation. Entry 21 is a separate compilation
|
||||||
|
refactor and does not solve this packaging problem.
|
||||||
|
|
||||||
|
Sources: [Correctness 2](01-correctness.md#2-external-schema-resources-do-not-reach-the-provider-constraint),
|
||||||
|
[Testing 5](08-testing.md#5-multi-resource-schema-tests-do-not-establish-a-usable-provider-constraint).
|
||||||
|
|
||||||
|
## 7. Preserve The Owning Operation's Public Error Category
|
||||||
|
|
||||||
|
**Confirmed classification defect.** Public error conversion searches nested
|
||||||
|
causes in a global priority order, allowing a collaborator's sentinel to hide
|
||||||
|
the enclosing operation's category. Map the owning boundary independently
|
||||||
|
while retaining the original cause for `errors.Is`/`errors.As`. Add composed
|
||||||
|
cause regressions; simply reordering global checks can break legitimate direct
|
||||||
|
not-found versus load-failure distinctions.
|
||||||
|
|
||||||
|
Sources: [Error handling 1](06-error-handling.md#1-nested-collaborator-errors-can-hide-the-owning-operations-public-category),
|
||||||
|
[Testing 4](08-testing.md#4-error-tests-miss-composed-causes-and-pre-dispatch-diagnostic-failures).
|
||||||
|
|
||||||
|
## 8. Distinguish Response Stream Failures From Malformed JSON
|
||||||
|
|
||||||
|
**Confirmed error-handling defect.** Non-context I/O failures while decoding a
|
||||||
|
successful HTTP response or checking its end are reported as malformed JSON,
|
||||||
|
losing the transport cause; complete JSON followed by a read failure can also
|
||||||
|
be misreported as trailing data. Retain and classify reader failures separately
|
||||||
|
using the existing redacted transport boundary. Preserve cancellation
|
||||||
|
precedence and genuine malformed/truncated/oversized-content handling. Cover
|
||||||
|
both read locations without adding transport retries.
|
||||||
|
|
||||||
|
Sources: [Error handling 2](06-error-handling.md#2-successful-status-body-read-failures-are-misreported-as-malformed-json),
|
||||||
|
[Testing 4](08-testing.md#4-error-tests-miss-composed-causes-and-pre-dispatch-diagnostic-failures).
|
||||||
|
|
||||||
|
## 9. Return Invalid Diagnostic Request Construction As A Go Error
|
||||||
|
|
||||||
|
**Confirmed diagnostic-contract defect.** Some invalid inference settings fail
|
||||||
|
before dispatch yet produce a completed report with a nil Go error. Propagate
|
||||||
|
local request-construction failures through `ErrInvalidRequest` and add a
|
||||||
|
no-dispatch regression. Preserve intentionally completed diagnostic outcomes
|
||||||
|
for missing credentials, capacity outcomes, and unsupported or inapplicable
|
||||||
|
operations; not every no-dispatch outcome is invalid configuration.
|
||||||
|
|
||||||
|
Sources: [Error handling 3](06-error-handling.md#3-inference-diagnostics-swallow-some-invalid-request-construction-errors),
|
||||||
|
[Testing 4](08-testing.md#4-error-tests-miss-composed-causes-and-pre-dispatch-diagnostic-failures).
|
||||||
|
|
||||||
|
## 10. Remove Quadratic Requirement-Evidence Union Work
|
||||||
|
|
||||||
|
**Confirmed algorithmic inefficiency.** A left-heavy recognized condition can
|
||||||
|
repeatedly copy growing missing-key evidence sets, producing quadratic work.
|
||||||
|
Use operation-local accumulation or ownership-aware merging while preserving
|
||||||
|
classification and conservative inspection coverage. Do not address this by
|
||||||
|
adding expression limits or expanding the recognized template language. Entry
|
||||||
|
23's traversal cleanup is related but is not a substitute for fixing this
|
||||||
|
specific accumulation algorithm.
|
||||||
|
|
||||||
|
Sources: [Efficiency 1](02-efficiency.md#1-condition-evidence-unions-can-require-quadratic-work).
|
||||||
|
|
||||||
|
## 11. Exercise RunRequest's Privacy Formatting Boundary
|
||||||
|
|
||||||
|
**Missing protection, not a demonstrated leak.** Request formatting methods
|
||||||
|
have no execution coverage despite owning a privacy-sensitive public boundary.
|
||||||
|
Add focused tests with distinct synthetic private markers for the relevant
|
||||||
|
value/pointer formatting paths and JSON omission behavior. Assert privacy and
|
||||||
|
the documented boundary, not an incidental complete diagnostic or JSON shape.
|
||||||
|
|
||||||
|
Sources: [Testing 1](08-testing.md#1-runrequests-privacy-formatting-contract-is-not-exercised).
|
||||||
|
|
||||||
|
## 12. Correct Profile Inspection Credential Documentation
|
||||||
|
|
||||||
|
**Public-contract documentation defect.** Inspection GoDoc incorrectly makes
|
||||||
|
required credentials and an environment variable mutually exclusive. Actual
|
||||||
|
behavior permits an explicit profile environment variable while suppressing
|
||||||
|
only the backend-derived default when required credentials are selected.
|
||||||
|
Correct the field/method GoDoc and consumer example, keeping the established
|
||||||
|
configuration behavior and DTO shape.
|
||||||
|
|
||||||
|
Sources: [Interfaces 2](09-interfaces.md#2-profile-inspection-misstates-required-credential-semantics).
|
||||||
|
|
||||||
|
## 13. Correct The Configured Provider-Map Wire Documentation
|
||||||
|
|
||||||
|
**Integration-contract documentation defect.** The wire document says backend
|
||||||
|
and profile provider maps replace one another, but configuration recursively
|
||||||
|
overlays them; runtime replacement is a separate rule. Align the integration
|
||||||
|
summary with the canonical format reference rather than maintaining a second
|
||||||
|
conflicting precedence definition. Entry 19 concerns the efficiency of the
|
||||||
|
existing overlay, not a change to its semantics.
|
||||||
|
|
||||||
|
Sources: [Interfaces 3](09-interfaces.md#3-the-wire-contract-incorrectly-says-configured-provider-maps-replace).
|
||||||
|
|
||||||
|
## 14. Remove Stale Identity And Version GoDoc
|
||||||
|
|
||||||
|
**Public-contract documentation defect.** Inspection comments still describe
|
||||||
|
trimmed profile IDs, and listing comments describe a removed prompt-version
|
||||||
|
field. Update them to the implemented literal identity and current collection
|
||||||
|
version model. Do not normalize meaningful whitespace or reintroduce a field
|
||||||
|
to match obsolete comments.
|
||||||
|
|
||||||
|
Sources: [Interfaces 4](09-interfaces.md#4-identity-godoc-still-describes-removed-normalization-and-version-fields).
|
||||||
|
|
||||||
|
## 15. Cover The Public Single-Schema-File Option
|
||||||
|
|
||||||
|
**Public wiring coverage gap.** Direct filesystem-validator tests do not
|
||||||
|
exercise `WithSchemaFile` through engine construction. Add offline temporary
|
||||||
|
file cases through public preparation/validation, including option precedence
|
||||||
|
and relevant file errors. This is a supported feature, not dead code; entry
|
||||||
|
32 removes only an unused private helper result.
|
||||||
|
|
||||||
|
Sources: [Testing 2](08-testing.md#2-the-public-single-schema-file-option-has-no-execution-coverage).
|
||||||
|
|
||||||
|
## 16. Detach Inert Prepared Handles From The Execution Owner
|
||||||
|
|
||||||
|
**Avoidable retention.** Used or discarded prepared handles retain their
|
||||||
|
runner, and therefore the engine's captured configuration and collaborators.
|
||||||
|
Use a narrower ownership identity or clear the unnecessary owner reference
|
||||||
|
safely. Preserve foreign-owner rejection, one-time claim, namespace-view
|
||||||
|
behavior, and available details. This is unnecessary retention when a handle
|
||||||
|
outlives its engine, not evidence of an ever-growing global leak.
|
||||||
|
|
||||||
|
Sources: [Resources 3](05-resources.md#3-inert-prepared-handles-retain-their-entire-execution-owner).
|
||||||
|
|
||||||
|
## 17. Bound Repair Feedback's Temporary Serialization
|
||||||
|
|
||||||
|
**Avoidable peak allocation.** Feedback formatting serializes potentially
|
||||||
|
large diagnostic text before truncating the final message, so its existing
|
||||||
|
output bound does not bound scratch memory. Produce bounded encoded feedback
|
||||||
|
incrementally, preserving the existing output semantics and complete stored
|
||||||
|
validation diagnostics and previous candidate. A new raw-diagnostic input
|
||||||
|
limit is not implied.
|
||||||
|
|
||||||
|
Sources: [Resources 4](05-resources.md#4-repair-feedbacks-output-bound-does-not-bound-its-temporary-serialization).
|
||||||
|
|
||||||
|
## 18. Detach Normalized Session IDs From Large Input Strings
|
||||||
|
|
||||||
|
**Avoidable retention.** A short trimmed session ID can retain the allocation
|
||||||
|
of a much larger padded caller string. Clone the accepted normalized value at
|
||||||
|
its ownership boundary. Preserve trimming, accepted content, code-point
|
||||||
|
validation, and resulting wire/hash content rather than adding a raw-input
|
||||||
|
size restriction.
|
||||||
|
|
||||||
|
Sources: [Resources 5](05-resources.md#5-short-normalized-session-ids-can-pin-large-input-strings).
|
||||||
|
|
||||||
|
## 19. Avoid Repeated Descendant Cloning During Provider Overlay
|
||||||
|
|
||||||
|
**Construction-time inefficiency.** Recursive overlays repeatedly clone and
|
||||||
|
sort overlapping descendant maps, giving work proportional to tree size times
|
||||||
|
depth. Existing depth bounds mean this is not unbounded quadratic growth.
|
||||||
|
Clone ownership once or use a single traversal while retaining recursive-map,
|
||||||
|
null, and array semantics. Coordinate with entry 24 without assuming all JSON
|
||||||
|
walkers have interchangeable policies.
|
||||||
|
|
||||||
|
Sources: [Efficiency 2](02-efficiency.md#2-recursive-provider-overlay-repeatedly-clones-descendant-subtrees).
|
||||||
|
|
||||||
|
## 20. Index Filtered Listings By Namespace
|
||||||
|
|
||||||
|
**Repeated inspection overhead.** Filtered listing currently copies and scans
|
||||||
|
the complete inventory even when few or no entries match. Capture namespace
|
||||||
|
buckets or equivalent ranges so queries operate on the relevant subset.
|
||||||
|
Preserve ordering, exact identity matching, cancellation, and caller-owned
|
||||||
|
results. A pagination API or runtime cache is not needed for this improvement.
|
||||||
|
|
||||||
|
Sources: [Efficiency 3](02-efficiency.md#3-filtered-listings-copy-and-scan-the-complete-inventory).
|
||||||
|
|
||||||
|
## 21. Share Schema Compilation And Declare The Runner's Real Dependency
|
||||||
|
|
||||||
|
**Maintainability and interface clarity.** Four host-file/`fs.FS`,
|
||||||
|
direct/prepared paths repeat the schema compilation lifecycle. The runner also
|
||||||
|
declares a validator interface while requiring preparation through a runtime
|
||||||
|
assertion. Share the lifecycle behind source-specific adapters and make that
|
||||||
|
dependency explicit. Preserve malformed-output short-circuiting before schema
|
||||||
|
I/O, source containment, exact numbers, cancellation, and frozen plans. Retain
|
||||||
|
independent cross-source tests; do not force whole entry points to be identical.
|
||||||
|
|
||||||
|
Sources: [Complexity 1](03-complexity.md#1-consolidate-schema-compilation-and-express-the-runners-actual-interface),
|
||||||
|
[Equivalence 1](10-equivalence.md#1-share-the-schema-compilation-lifecycle-across-source-and-execution-paths).
|
||||||
|
|
||||||
|
## 22. Decompose Configuration Validation And Materialization
|
||||||
|
|
||||||
|
**Maintainability.** Large configuration operations mix record-specific
|
||||||
|
validation, resource capture, and effective-profile construction. Extract
|
||||||
|
focused helpers with visible inputs and ownership, rather than another generic
|
||||||
|
validation framework. Preserve final-handoff validation, presence/null rules,
|
||||||
|
origins, and meaningful fail-fast/error-precedence behavior. Do not turn
|
||||||
|
incidental ordering of independent validation failures into a new guarantee. The configuration-boundary ADR remains
|
||||||
|
authoritative; this is not a proposal to inspect raw ConfigKit sources.
|
||||||
|
|
||||||
|
Sources: [Complexity 2](03-complexity.md#2-decompose-configuration-validation-and-resource-materialization).
|
||||||
|
|
||||||
|
## 23. Clarify Requirement Traversal And Share Branch Orchestration
|
||||||
|
|
||||||
|
**Maintainability.** Separate recognition/evaluation from reachability and
|
||||||
|
evidence bookkeeping, particularly the repeated `if`/`with` branch protocol.
|
||||||
|
Keep their scope differences explicit and retain independent comparisons with
|
||||||
|
the actual Go renderer. Preserve conservative partial coverage and inactive
|
||||||
|
computed-name behavior. This does not justify a general template interpreter
|
||||||
|
or stronger execution-readiness promises from requirements inspection.
|
||||||
|
|
||||||
|
Sources: [Complexity 3](03-complexity.md#3-untangle-requirement-traversal-expression-evaluation-and-evidence),
|
||||||
|
[Equivalence 3](10-equivalence.md#3-share-ifwith-requirement-branch-orchestration-not-template-execution).
|
||||||
|
|
||||||
|
## 24. Centralize Compatible JSON Copying Without Conflating Policies
|
||||||
|
|
||||||
|
**Maintainability and ownership clarity.** The facade maintains a separate
|
||||||
|
reflection traversal for already-normalized values despite an existing
|
||||||
|
`internal/jsonvalue` owner. Consolidate genuinely compatible structural
|
||||||
|
copying while retaining DTO projection at the facade and distinct source
|
||||||
|
encoding/runtime validation policies. Preserve concrete types, nils, exact
|
||||||
|
numbers, schema keys, and ownership. Infallible projection must not acquire
|
||||||
|
new errors, panics, or silent zero-value fallbacks. Entries 3 and 5 remain
|
||||||
|
separate source-conversion defects, not reasons to reject pre-handoff sources.
|
||||||
|
|
||||||
|
Sources: [Complexity 4](03-complexity.md#4-clarify-ownership-and-policies-across-the-json-value-walkers),
|
||||||
|
[Equivalence 2](10-equivalence.md#2-centralize-structural-copying-of-already-normalized-json-values).
|
||||||
|
|
||||||
|
## 25. Separate Attempt Accounting From Generation Control Flow
|
||||||
|
|
||||||
|
**Maintainability.** Generation coordination interleaves dispatch/retry policy
|
||||||
|
with parallel return values and repeated evidence/observation projection.
|
||||||
|
Introduce a small attempt result and focused accounting helpers. Preserve
|
||||||
|
built-in versus injected-client differences, permit release before callbacks
|
||||||
|
or backoff, frozen requests, existing retry classification, private evidence,
|
||||||
|
and known/unknown usage distinctions. Do not collapse legitimately different
|
||||||
|
dispatch strategies merely to reduce lines.
|
||||||
|
|
||||||
|
Sources: [Complexity 5](03-complexity.md#5-extract-attempt-accounting-from-generation-control-flow).
|
||||||
|
|
||||||
|
## 26. Move OpenRouter Authentication Protocol Details Into Its Adapter
|
||||||
|
|
||||||
|
**Architectural ownership.** Use-case code currently interprets OpenRouter's
|
||||||
|
account-response fields and provider-relative endpoint while the outbound
|
||||||
|
adapter owns HTTP execution. Place protocol construction/recognition with the
|
||||||
|
outbound integration; keep diagnostic selection, applicability, cancellation,
|
||||||
|
completion, and neutral reporting in the use case. Preserve credential/body
|
||||||
|
privacy and anonymous probing. No plugin framework, reverse dependency, or
|
||||||
|
blanket prohibition on HTTP facts is needed. Coordinate the body-bound fix
|
||||||
|
in entry 2, which is a distinct behavioral change.
|
||||||
|
|
||||||
|
Sources: [Boundaries 2](11-boundaries.md#2-put-openrouter-authentication-protocol-details-behind-the-outbound-adapter).
|
||||||
|
|
||||||
|
## 27. Narrow Smoke-Value Copy Helpers To Real Consumers
|
||||||
|
|
||||||
|
**Ownership cleanup.** A request cloner has only its self-test as a caller;
|
||||||
|
the full-report cloner is used to detach only a target. Express that actual
|
||||||
|
copy operation directly and retire unused broader cloning after checking
|
||||||
|
callers. Keep facade request/report projection, active backend cloning,
|
||||||
|
provider-map errors, ownership tests, and redaction. Do not introduce shared
|
||||||
|
public/internal DTOs or an additional full-report copy.
|
||||||
|
|
||||||
|
Sources: [Boundaries 3](11-boundaries.md#3-narrow-smoke-value-copy-helpers-to-their-actual-ownership-boundaries).
|
||||||
|
|
||||||
|
## 28. Give Bundled Catalog Expectations One Test Owner
|
||||||
|
|
||||||
|
**Test duplication.** Complete bundled profile/model/provider expectations are
|
||||||
|
repeated across several test layers. Keep one independently specified asset
|
||||||
|
oracle, with smaller tests for distinct loading, wire, and diagnostic behavior.
|
||||||
|
Use controlled fixtures where bundled churn is irrelevant and structural
|
||||||
|
assertions rather than formatted-map strings. Retain meaningful bundled
|
||||||
|
coverage instead of deriving every expected value from the loader under test.
|
||||||
|
|
||||||
|
Sources: [Testing 7](08-testing.md#7-bundled-catalog-expectations-have-multiple-test-owners).
|
||||||
|
|
||||||
|
## 29. Stop Pinning The Opaque Artifact Hash Algorithm In Tests
|
||||||
|
|
||||||
|
**Over-specified tests.** Artifact tests and a benchmark assert a literal hash
|
||||||
|
algorithm despite the public value being opaque. Assert the documented
|
||||||
|
equality/ownership relationships and actual byte/size behavior instead; keep
|
||||||
|
benchmark setup outside its timed work. Coordinate with entry 31, but do not
|
||||||
|
turn today's hash representation into a new compatibility guarantee.
|
||||||
|
|
||||||
|
Sources: [Testing 6](08-testing.md#6-artifact-tests-and-benchmarks-pin-an-explicitly-opaque-hash-algorithm).
|
||||||
|
|
||||||
|
## 30. Remove Unconsumed Backend Presence Bookkeeping
|
||||||
|
|
||||||
|
**Low-priority state cleanup.** Backend field-state maps and
|
||||||
|
`QueueCapacitySet` have no runtime reader; effective values and ConfigKit
|
||||||
|
already own queue defaulting and provenance. Remove only these unused records
|
||||||
|
and adjust tests to assert effective behavior or operator inspection. Keep
|
||||||
|
profile/input presence tracking, explicit zero capacity, and derived defaults.
|
||||||
|
|
||||||
|
Sources: [Dead code 1](12-deadcode.md#1-remove-backend-presence-bookkeeping-with-no-runtime-consumer).
|
||||||
|
|
||||||
|
## 31. Retire Duplicate Fields In Captured Prompt Definitions
|
||||||
|
|
||||||
|
**Low-priority representation cleanup.** Captured definitions duplicate the
|
||||||
|
output format and carry an unused runtime message-file field after content
|
||||||
|
materialization. Use the captured output contract and text as their respective
|
||||||
|
sources of truth. Preserve real YAML `content_file` loading, origins, and the
|
||||||
|
request-override default rule. Removing serialized internal fields changes
|
||||||
|
current definition hashes: retain documented opaque equality relationships,
|
||||||
|
not an invented bit-identical encoding promise.
|
||||||
|
|
||||||
|
Sources: [Dead code 2](12-deadcode.md#2-retire-legacy-fields-in-the-captured-prompt-representation).
|
||||||
|
|
||||||
|
## 32. Remove Unused Private Diagnostic State And Helper Results
|
||||||
|
|
||||||
|
**Low-priority cleanup.** A private reachability outcome echoes an endpoint
|
||||||
|
that report assembly never reads; the schema-file helper returns a directory
|
||||||
|
that its only caller discards. Remove the unused state/result while preserving
|
||||||
|
actual sanitized request/report evidence, directory calculation, file checks,
|
||||||
|
and public single-file schema support. Keep privacy tests against the values
|
||||||
|
that real execution consumes.
|
||||||
|
|
||||||
|
Sources: [Dead code 3](12-deadcode.md#3-remove-unused-diagnostic-state-and-a-discarded-helper-result).
|
||||||
|
|
||||||
|
## 33. Remove Orphan Internal Helpers And Their Self-Tests
|
||||||
|
|
||||||
|
**Low-priority cleanup.** `filecatalog.RelativePath` and
|
||||||
|
`resourceNamespaceFromID` have only their own tests as callers, and the runner
|
||||||
|
test's `stringPtr` helper is entirely unused. Remove those narrow surfaces
|
||||||
|
without replacement, preserving real discovery, identity qualification, and
|
||||||
|
namespace inventory/filtering coverage. Reachability from examples alone is
|
||||||
|
not a reason to remove optional public APIs.
|
||||||
|
|
||||||
|
Sources: [Dead code 4](12-deadcode.md#4-remove-orphan-helpers-whose-only-callers-are-their-own-tests).
|
||||||
|
|
||||||
|
## 34. Decide Whether To Retain The Legacy Capacity Client Wrapper
|
||||||
|
|
||||||
|
**Decision-dependent architectural cleanup.** The old capacity client wrapper
|
||||||
|
is used by tests, whereas production dispatch uses the generation coordinator.
|
||||||
|
It is also explicitly retained in current internal documentation; the dead-code
|
||||||
|
audit does not classify it as obsolete functionality. If that retention decision
|
||||||
|
is revised, exercise the production coordinator in runner tests and move any
|
||||||
|
needed one-shot capacity scaffolding into tests before retiring the wrapper.
|
||||||
|
Preserve FIFO, cancellation, independent-pool, and panic-release coverage.
|
||||||
|
This is not a second scheduling algorithm or a confirmed concurrency defect.
|
||||||
|
|
||||||
|
Sources: [Boundaries 1](11-boundaries.md#1-retire-the-test-only-capacity-client-as-a-production-scheduling-surface),
|
||||||
|
[Dead-code retention qualification](12-deadcode.md#intentional-retention-and-related-findings).
|
||||||
|
|
||||||
|
## Open Decisions
|
||||||
|
|
||||||
|
Work through the decisions below in this order. These are unresolved choices,
|
||||||
|
not additional findings or approved changes to public contracts. A decision
|
||||||
|
blocks its dependent remediation, not unrelated fixes. In particular, updating
|
||||||
|
the maintainer toolchain need not wait for the consumer-minimum decision.
|
||||||
|
Implementation details and optional cleanup do not become release gates merely
|
||||||
|
by appearing in this catalog.
|
||||||
|
|
||||||
|
### Decision 1: ArtifactReader Result Ownership And Lifetime
|
||||||
|
|
||||||
|
**Related finding:** [4](#4-resolve-the-artifactreader-buffer-ownership-contract).
|
||||||
|
|
||||||
|
**Question:** What ownership does a successful `Read` transfer, and when may
|
||||||
|
an injected reader mutate or reuse the returned storage?
|
||||||
|
|
||||||
|
**Recommendation:** Transfer an independent result to PromptKit on successful
|
||||||
|
return. The reader must stop mutating the returned artifact and its body,
|
||||||
|
including through retained aliases. Fresh per-invocation storage is the simplest
|
||||||
|
implementation; immutable shared data can remain shared if no collaborator
|
||||||
|
mutates it. This removes the impossible immediate-reuse permission without
|
||||||
|
adding synchronization or a release protocol to the interface. Explain the
|
||||||
|
consumer impact as a deliberate pre-1.0 contract correction.
|
||||||
|
|
||||||
|
**Alternative:** Permit borrowing until the enclosing preparation/execution
|
||||||
|
operation returns. This can support application-managed reuse, but a reader
|
||||||
|
cannot generally observe that lifetime and concurrent calls need independent
|
||||||
|
storage or caller coordination. If reuse immediately after `Read` returns is
|
||||||
|
required, an explicit acknowledgement/release API is another viable design;
|
||||||
|
it enables pooling at the cost of a larger interface and lifecycle change.
|
||||||
|
|
||||||
|
**Resolve before implementation:** Select the ownership/lifetime rule and update
|
||||||
|
GoDoc, adapter assumptions, and the regression scenario together. Another
|
||||||
|
unsynchronized copy is not a remedy for the current permission.
|
||||||
|
|
||||||
|
### Decision 2: Provider Submission Of Multi-Resource Schemas
|
||||||
|
|
||||||
|
**Related findings:** [6](#6-make-provider-native-schema-constraints-self-contained)
|
||||||
|
and [21](#21-share-schema-compilation-and-declare-the-runners-real-dependency).
|
||||||
|
|
||||||
|
**Question:** How should native structured output represent a schema whose
|
||||||
|
local validation depends on external resources?
|
||||||
|
|
||||||
|
**Recommendation:** First investigate packaging the operation's captured
|
||||||
|
resource graph into a self-contained provider constraint. Check identifiers,
|
||||||
|
relative references, anchors, recursion, and supported dialects before choosing
|
||||||
|
an encoding. Prefer correct packaging when it fits a proportionate scope,
|
||||||
|
while keeping resource completeness distinct from any provider's supported
|
||||||
|
schema subset. A standalone offline validation of the actual emitted constraint
|
||||||
|
should establish the selected packaging behavior.
|
||||||
|
|
||||||
|
**Alternative:** Offer an explicit local-only validation choice when native
|
||||||
|
submission cannot represent the resource graph. This preserves useful local
|
||||||
|
validation and avoids building a general schema transformation system, but
|
||||||
|
forgoes native generation constraints for that selected mode and needs a clear
|
||||||
|
consumer-facing selection mechanism. Silently dropping native constraints is
|
||||||
|
not equivalent to an explicit choice.
|
||||||
|
|
||||||
|
**Resolve before implementation:** Establish packaging feasibility and select
|
||||||
|
the submission/fallback policy, including behavior for unsupported cases.
|
||||||
|
Coordinate the compilation refactor with that result; it does not itself fix
|
||||||
|
provider packaging or establish universal provider acceptance.
|
||||||
|
|
||||||
|
### Decision 3: Authentication Response Budget And Overflow Outcome
|
||||||
|
|
||||||
|
**Related findings:** [2](#2-bound-authentication-probe-response-consumption)
|
||||||
|
and [26](#26-move-openrouter-authentication-protocol-details-into-its-adapter).
|
||||||
|
|
||||||
|
**Question:** What finite response budget should the authentication adapter
|
||||||
|
apply, and how should a probe report exceeding it?
|
||||||
|
|
||||||
|
**Recommendation:** Select a conservative fixed budget for this small protocol,
|
||||||
|
check actual streamed bytes regardless of declared length, and close on overflow
|
||||||
|
without unbounded draining. Report the diagnostic failure with acceptance
|
||||||
|
unknown, retaining any independently observed HTTP facts. Agree the exact byte
|
||||||
|
count and its representation in the existing diagnostic report before coding;
|
||||||
|
the catalog does not select either implicitly.
|
||||||
|
|
||||||
|
**Alternative:** Expose a caller-configurable budget with a finite default.
|
||||||
|
This accommodates unusual legitimate responses but expands configuration and
|
||||||
|
validation surface. It is worthwhile if a concrete consumer requirement cannot
|
||||||
|
be served by an appropriately chosen fixed budget; it is not necessary merely
|
||||||
|
to address the demonstrated unbounded read.
|
||||||
|
|
||||||
|
**Resolve before implementation:** Choose the size, overflow classification,
|
||||||
|
and whether configurability is warranted. Keep this fix independent of adapter
|
||||||
|
relocation so architectural cleanup does not delay the resource remedy.
|
||||||
|
|
||||||
|
### Decision 4: Exact Numbers And The Source-Conversion Boundary
|
||||||
|
|
||||||
|
**Related findings:** [3](#3-avoid-exponential-expansion-during-convenience-source-conversion),
|
||||||
|
[5](#5-preserve-exact-numbers-in-programmatic-provider-sources), and
|
||||||
|
[24](#24-centralize-compatible-json-copying-without-conflating-policies).
|
||||||
|
|
||||||
|
**Question:** Which exact numeric values can the convenience helpers preserve
|
||||||
|
through the current ConfigKit mapping boundary, and what happens to others?
|
||||||
|
|
||||||
|
**Recommendation:** Preserve supported values through the complete conversion,
|
||||||
|
load, and outbound-encoding path; defer an unsupported-value error when exact
|
||||||
|
preservation is unavailable. Compare numeric meaning rather than spelling or
|
||||||
|
successful parsing alone. Address shared-graph expansion in the same adapter
|
||||||
|
with completed-conversion memoization, distinct cycle tracking, and correct
|
||||||
|
slice-view identity, preserving source capture and deferred failures.
|
||||||
|
|
||||||
|
**Alternative:** Request upstream support for additional exact numeric/value
|
||||||
|
forms, then simplify or remove the relevant PromptKit conversion. This may
|
||||||
|
provide a cleaner long-term boundary and broader preservation, but introduces
|
||||||
|
an upstream dependency and should not postpone fixing silent rounding or
|
||||||
|
exponential expansion. Runtime JSON copying remains a separate policy.
|
||||||
|
|
||||||
|
**Resolve before implementation:** Establish the supported numeric round-trip
|
||||||
|
cases and whether broader upstream support is needed now. Do not substitute
|
||||||
|
numeric-looking strings or introduce a second raw-source validation system.
|
||||||
|
|
||||||
|
### Decision 5: Consumer Go Minimum After The Toolchain Update
|
||||||
|
|
||||||
|
**Related finding:** [1](#1-update-the-audited-maintainer-toolchain-for-applicable-security-fixes).
|
||||||
|
|
||||||
|
**Question:** Should the security fixes also change PromptKit's declared minimum
|
||||||
|
consumer Go version?
|
||||||
|
|
||||||
|
**Recommendation:** Update the maintainer toolchain and repeat the focused
|
||||||
|
assessment promptly; initially keep the consumer minimum decision separate.
|
||||||
|
Advise affected consumers to rebuild with a patched toolchain. This recognizes
|
||||||
|
that PromptKit publishes source and that downstream build/transport choices
|
||||||
|
control the deployed exposure.
|
||||||
|
|
||||||
|
**Alternative:** Raise the declared minimum to the selected patched release.
|
||||||
|
This makes the supported floor clearer and steers module consumers toward the
|
||||||
|
fixed toolchain, but changes compatibility and does not repair already-built
|
||||||
|
applications. Assess it under the existing release policy rather than treating
|
||||||
|
it as an automatic consequence of the scanner output.
|
||||||
|
|
||||||
|
**Resolve before changing the minimum:** Confirm current advisory applicability,
|
||||||
|
select the patched version, and decide whether changing the supported consumer
|
||||||
|
floor is justified. No new recurring release gate is implied.
|
||||||
|
|
||||||
|
### Decision 6: Retention Of The Legacy Capacity Client Wrapper
|
||||||
|
|
||||||
|
**Related finding:** [34](#34-decide-whether-to-retain-the-legacy-capacity-client-wrapper).
|
||||||
|
|
||||||
|
**Question:** Does the wrapper still justify a production surface for its
|
||||||
|
currently documented test-focused purpose?
|
||||||
|
|
||||||
|
**Recommendation:** Retire the production wrapper after moving the runner's
|
||||||
|
integration test to the production generation coordinator. Keep direct manager
|
||||||
|
coverage and any useful one-shot scaffolding in tests. This reduces maintenance
|
||||||
|
and makes assembled scheduling tests reflect production while preserving the
|
||||||
|
actual FIFO, cancellation, independent-pool, and panic-release scenarios.
|
||||||
|
|
||||||
|
**Alternative:** Retain it if a concrete internal use justifies the small
|
||||||
|
adapter. This avoids test migration and preserves that convenient composition,
|
||||||
|
but keeps a second assembly surface to understand and maintain. Existing
|
||||||
|
self-tests alone do not establish a future production need.
|
||||||
|
|
||||||
|
**Resolve before removal:** Confirm the intended retention decision and test
|
||||||
|
ownership. This cleanup can be deferred without blocking the behavioral fixes;
|
||||||
|
it is not evidence of a second scheduler or a concurrency defect.
|
||||||
@@ -42,20 +42,21 @@ checkpoint equality or when a broader semantic identity design is selected.
|
|||||||
### Eager Source Validation
|
### Eager Source Validation
|
||||||
|
|
||||||
**Reason for deferral:** Exact prompt and profile inspection may already
|
**Reason for deferral:** Exact prompt and profile inspection may already
|
||||||
provide a sufficiently small validation surface. Experience from downstream
|
provide a sufficiently small validation surface alongside construction-time
|
||||||
adoption should establish whether an engine-wide operation would add enough
|
definition validation. Experience from downstream adoption should establish
|
||||||
value to justify its broader contract.
|
whether a separate operation would add enough value to justify its broader
|
||||||
|
contract.
|
||||||
|
|
||||||
Promptkit could provide an explicit offline operation that discovers and
|
Promptkit could provide an explicit offline operation that also checks schema
|
||||||
structurally validates configured prompt, profile, and schema sources without
|
resources without model generation. `NewEngine` already validates effective
|
||||||
model generation. The normal `NewEngine` path would remain lazy.
|
definitions and captures prompt content; schema resources remain
|
||||||
|
request-dependent.
|
||||||
|
|
||||||
An eager operation would need coherent handling for duplicate prompt IDs and
|
Such an operation would need coherent handling for schema syntax and transitive
|
||||||
versions, strict YAML decoding, referenced content files, profile/backend
|
references, context cancellation, and source-specific public errors. Credential
|
||||||
membership, schema syntax and transitive references, context cancellation,
|
declarations must remain separate from credential values; checking current
|
||||||
and source-specific public errors. Credential declarations must remain
|
environment availability, if supported at all, should be an explicit option
|
||||||
separate from credential values; checking current environment availability,
|
and must not expose secrets.
|
||||||
if supported at all, should be an explicit option and must not expose secrets.
|
|
||||||
|
|
||||||
Reconsider this idea after downstream use of `InspectPrompt`,
|
Reconsider this idea after downstream use of `InspectPrompt`,
|
||||||
`InspectProfile`, and fixture-based preparation demonstrates a concrete gap.
|
`InspectProfile`, and fixture-based preparation demonstrates a concrete gap.
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ consumers.
|
|||||||
|
|
||||||
## Ideas
|
## Ideas
|
||||||
|
|
||||||
No ideas are currently awaiting selection. Active feature work belongs in its
|
No unselected ideas are currently recorded here. Deliberately postponed ideas
|
||||||
focused roadmap rather than this catalog.
|
remain in the [deferred catalog](deferred.md).
|
||||||
|
|
||||||
## Entry Format
|
## Entry Format
|
||||||
|
|
||||||
|
|||||||
301
engine.go
301
engine.go
@@ -8,14 +8,15 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
"go.maximumdirect.net/configkit"
|
||||||
internalengine "gitea.maximumdirect.net/eric/promptkit/internal/engine"
|
"go.maximumdirect.net/promptkit/internal/domain"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/observation"
|
internalengine "go.maximumdirect.net/promptkit/internal/engine"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/usecase"
|
"go.maximumdirect.net/promptkit/internal/observation"
|
||||||
|
"go.maximumdirect.net/promptkit/internal/usecase"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInvalidConfig identifies invalid engine construction, including missing
|
// ErrInvalidConfig identifies invalid engine construction, including missing
|
||||||
// required configuration, invalid options or backend registrations, and a nil
|
// effective configuration, invalid options, and a nil
|
||||||
// Engine receiver.
|
// Engine receiver.
|
||||||
var ErrInvalidConfig = errors.New("invalid engine configuration")
|
var ErrInvalidConfig = errors.New("invalid engine configuration")
|
||||||
|
|
||||||
@@ -23,9 +24,8 @@ var (
|
|||||||
// ErrInvalidRequest identifies a request whose required values, overrides,
|
// ErrInvalidRequest identifies a request whose required values, overrides,
|
||||||
// credentials, or effective settings are invalid.
|
// credentials, or effective settings are invalid.
|
||||||
ErrInvalidRequest = errors.New("invalid run request")
|
ErrInvalidRequest = errors.New("invalid run request")
|
||||||
// ErrPromptNotFound identifies a requested prompt ID or version that is not
|
// ErrPromptNotFound identifies a requested prompt ID that is not present in
|
||||||
// present in the selected prompt source. It does not also match
|
// the captured configuration. It does not also match ErrPromptLoad.
|
||||||
// ErrPromptLoad.
|
|
||||||
ErrPromptNotFound = errors.New("prompt not found")
|
ErrPromptNotFound = errors.New("prompt not found")
|
||||||
// ErrProfileNotFound identifies a selected profile ID that is absent from
|
// ErrProfileNotFound identifies a selected profile ID that is absent from
|
||||||
// every configured profile source. It does not also match ErrProfileLoad.
|
// every configured profile source. It does not also match ErrProfileLoad.
|
||||||
@@ -34,13 +34,13 @@ var (
|
|||||||
// nor the selected prompt's default profile is present. Such an error also
|
// nor the selected prompt's default profile is present. Such an error also
|
||||||
// matches ErrInvalidRequest.
|
// matches ErrInvalidRequest.
|
||||||
ErrProfileRequired = errors.New("profile selection is required")
|
ErrProfileRequired = errors.New("profile selection is required")
|
||||||
// ErrPromptLoad identifies a failure to read, decode, validate, select, or
|
// ErrPromptLoad identifies a prompt selection failure other than the
|
||||||
// hash a prompt definition, except for the not-found case represented by
|
// not-found case represented by ErrPromptNotFound. Invalid definitions
|
||||||
// ErrPromptNotFound.
|
// instead fail construction with ErrInvalidConfig.
|
||||||
ErrPromptLoad = errors.New("failed to load prompt definition")
|
ErrPromptLoad = errors.New("failed to load prompt definition")
|
||||||
// ErrProfileLoad identifies a failure to read, decode, validate, or select
|
// ErrProfileLoad identifies a profile selection or resolution failure other
|
||||||
// an execution profile or resolve its backend, except for the profile
|
// than the not-found case represented by ErrProfileNotFound. Invalid
|
||||||
// not-found case represented by ErrProfileNotFound.
|
// definitions instead fail construction with ErrInvalidConfig.
|
||||||
ErrProfileLoad = errors.New("failed to load execution profile")
|
ErrProfileLoad = errors.New("failed to load execution profile")
|
||||||
// ErrAPIKeyEnvMissing identifies an explicitly required APIKeyEnv whose
|
// ErrAPIKeyEnvMissing identifies an explicitly required APIKeyEnv whose
|
||||||
// environment variable is unset or empty after direct RunRequest.APIKey
|
// environment variable is unset or empty after direct RunRequest.APIKey
|
||||||
@@ -81,31 +81,41 @@ var (
|
|||||||
ErrValidation = errors.New("failed to validate output")
|
ErrValidation = errors.New("failed to validate output")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Engine inspects prompts and profiles and prepares and runs Promptkit prompt
|
// Engine inspects prompts and profiles, enumerates prompt requirements, and
|
||||||
// requests.
|
// prepares and runs Promptkit prompt requests.
|
||||||
//
|
//
|
||||||
// An Engine is safe for concurrent calls to [Engine.InspectPrompt],
|
// An Engine is safe for concurrent calls to [Engine.WithNamespaces],
|
||||||
// [Engine.InspectProfile], [Engine.Prepare], [Engine.PrepareExecution],
|
// [Engine.InspectConfiguration],
|
||||||
// [Engine.Run], and [Engine.RunPrepared]. Each Engine owns independent
|
// [Engine.InspectPrompt],
|
||||||
// backend-capacity pools that coordinate Run and RunPrepared admission and
|
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.Prepare], [Engine.PrepareExecution],
|
||||||
// model generation. Injected collaborators may still be invoked concurrently
|
// [Engine.Run], [Engine.RunPrepared], [Engine.SmokeTest], [Engine.ListProfiles],
|
||||||
// across different backend pools or for unlimited backends.
|
// [Engine.ListPrompts], [Engine.ListBackends], [Engine.ListNamespaces], and
|
||||||
|
// [Engine.ListCollections]. Engines constructed separately by [NewEngine] own
|
||||||
|
// independent backend-capacity pools that coordinate Run, RunPrepared, and
|
||||||
|
// inference SmokeTest admission and model generation. Namespace views returned
|
||||||
|
// by [Engine.WithNamespaces] share their underlying engine's pools, clients,
|
||||||
|
// and prepared-execution owner. Injected collaborators may still be invoked
|
||||||
|
// concurrently across different backend pools or for unlimited backends.
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
internal *internalengine.Engine
|
internal *internalengine.Engine
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config selects the directory-backed sources and built-in model-client
|
// ConfigurationSources selects ordered ConfigKit underlays and an optional
|
||||||
// transport used by [NewEngine]. Config has no stable JSON representation.
|
// highest-priority main input. By default a bundled OpenRouter underlay precedes
|
||||||
|
// the caller underlays; [WithBundledOpenRouter] can disable it. Inputs within
|
||||||
|
// each underlay strictly compose; later underlays overlay earlier ones. A nil Main uses
|
||||||
|
// ConfigKit's empty source. A non-nil Main is passed through unchanged and
|
||||||
|
// may fail construction if invalid.
|
||||||
|
type ConfigurationSources struct {
|
||||||
|
Underlays []configkit.Underlay
|
||||||
|
Main *configkit.Input
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config selects configuration sources and built-in model-client transport
|
||||||
|
// used by [NewEngine]. Config has no stable JSON representation.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// PromptDir is the directory searched recursively for prompt definitions.
|
// Sources combines maintained definitions with caller-selected inputs.
|
||||||
// It is required unless a WithPromptFS or WithPromptFile option supplies the
|
Sources ConfigurationSources
|
||||||
// prompt source.
|
|
||||||
PromptDir string
|
|
||||||
// ProfileDir is an optional ordinary configured source whose profiles take
|
|
||||||
// precedence over application fallback and maintained catalog profiles. An
|
|
||||||
// empty value selects the lower-precedence sources unless a profile-source
|
|
||||||
// option supplies the ordinary source.
|
|
||||||
ProfileDir string
|
|
||||||
// SchemaDir is the root for JSON Schema files. An empty value uses the
|
// SchemaDir is the root for JSON Schema files. An empty value uses the
|
||||||
// current directory. WithSchemaFS or WithSchemaFile replaces this source.
|
// current directory. WithSchemaFS or WithSchemaFile replaces this source.
|
||||||
SchemaDir string
|
SchemaDir string
|
||||||
@@ -128,12 +138,9 @@ type Config struct {
|
|||||||
// Option customizes engine construction.
|
// Option customizes engine construction.
|
||||||
//
|
//
|
||||||
// NewEngine applies options in argument order and ignores nil options. Within
|
// NewEngine applies options in argument order and ignores nil options. Within
|
||||||
// each prompt-source, ordinary-profile-source, fallback-profile-source,
|
// each schema-source, model-client, and artifact-reader category, the last
|
||||||
// in-memory-profile, schema-source, model-client, and artifact-reader
|
// non-nil valid option replaces earlier options. An invalid option fails
|
||||||
// category, the last non-nil valid option replaces earlier options in that
|
// construction even if a later option would replace it.
|
||||||
// category. WithBackend is the additive exception: unique registrations
|
|
||||||
// accumulate, and a repeated backend ID is an error rather than a replacement.
|
|
||||||
// An invalid option fails construction even if a later option would replace it.
|
|
||||||
type Option interface {
|
type Option interface {
|
||||||
apply(*engineOptions) error
|
apply(*engineOptions) error
|
||||||
}
|
}
|
||||||
@@ -148,6 +155,15 @@ type engineOptions struct {
|
|||||||
internal internalengine.Options
|
internal internalengine.Options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithBundledOpenRouter enables or disables the built-in OpenRouter underlay.
|
||||||
|
// It is enabled by default; when repeated, the last option wins.
|
||||||
|
func WithBundledOpenRouter(enabled bool) Option {
|
||||||
|
return optionFunc(func(options *engineOptions) error {
|
||||||
|
options.internal.SetBundledOpenRouter(enabled)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// WithLLMClient replaces the built-in model client used by [Engine.Run] and
|
// WithLLMClient replaces the built-in model client used by [Engine.Run] and
|
||||||
// [Engine.RunPrepared].
|
// [Engine.RunPrepared].
|
||||||
//
|
//
|
||||||
@@ -181,102 +197,6 @@ func WithArtifactReader(reader ArtifactReader) Option {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithPromptFS loads prompt definitions from fsys under root.
|
|
||||||
//
|
|
||||||
// The source uses the same strict prompt YAML rules as configured prompt
|
|
||||||
// directories, and prompt content_file paths resolve within this source.
|
|
||||||
// fsys must be non-nil and root must be non-empty; otherwise NewEngine fails
|
|
||||||
// with ErrInvalidConfig. This option replaces Config.PromptDir and earlier
|
|
||||||
// prompt-source options.
|
|
||||||
func WithPromptFS(fsys fs.FS, root string) Option {
|
|
||||||
return optionFunc(func(options *engineOptions) error {
|
|
||||||
return options.internal.SetPromptFS(fsys, root)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithPromptFile loads prompt definitions from the single prompt file at path.
|
|
||||||
//
|
|
||||||
// Relative prompt content_file paths resolve from the file's directory. path
|
|
||||||
// must name an existing non-directory file when NewEngine applies the option.
|
|
||||||
// This option replaces Config.PromptDir and earlier prompt-source options.
|
|
||||||
func WithPromptFile(path string) Option {
|
|
||||||
return optionFunc(func(options *engineOptions) error {
|
|
||||||
return options.internal.SetPromptFile(path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithProfileFS loads execution profiles from fsys under root.
|
|
||||||
//
|
|
||||||
// Profiles from this ordinary configured source take precedence over
|
|
||||||
// application fallback and built-in profiles. Profile YAML must use api_key_env
|
|
||||||
// for environment-based credentials; raw API keys are rejected. fsys must be
|
|
||||||
// non-nil and root must be non-empty; otherwise NewEngine fails with
|
|
||||||
// ErrInvalidConfig. This option replaces Config.ProfileDir and earlier file or
|
|
||||||
// FS profile-source options, but remains below WithProfiles in precedence.
|
|
||||||
func WithProfileFS(fsys fs.FS, root string) Option {
|
|
||||||
return optionFunc(func(options *engineOptions) error {
|
|
||||||
return options.internal.SetProfileFS(fsys, root)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithProfileFile loads execution profiles from the single profile file at path.
|
|
||||||
//
|
|
||||||
// The profile takes precedence over application fallback and built-in profiles.
|
|
||||||
// Profile YAML must use api_key_env for environment-based credentials; raw API
|
|
||||||
// keys are rejected. path must name an existing non-directory file when
|
|
||||||
// NewEngine applies the option. This option replaces Config.ProfileDir and
|
|
||||||
// earlier file or FS profile-source options, but remains below WithProfiles in
|
|
||||||
// precedence.
|
|
||||||
func WithProfileFile(path string) Option {
|
|
||||||
return optionFunc(func(options *engineOptions) error {
|
|
||||||
return options.internal.SetProfileFile(path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFallbackProfileFS supplies application-owned fallback profile
|
|
||||||
// definitions from fsys under root.
|
|
||||||
//
|
|
||||||
// Profile lookup checks, in order, profiles supplied by WithProfiles; the
|
|
||||||
// ordinary configured source selected by WithProfileFile, WithProfileFS, or
|
|
||||||
// Config.ProfileDir; this fallback source; and Promptkit's maintained catalog
|
|
||||||
// profiles. Each source supplies a complete profile definition; profile fields
|
|
||||||
// are not merged between sources. Only an absent profile ID proceeds to the
|
|
||||||
// next source. A matching read, parse, duplicate, validation, or credential
|
|
||||||
// format failure stops resolution.
|
|
||||||
//
|
|
||||||
// Files use the ordinary strict profile YAML and api_key_env credential rules.
|
|
||||||
// Loading and validation are lazy: NewEngine validates this option's arguments
|
|
||||||
// but does not read profile files. fsys must be non-nil and root must be
|
|
||||||
// nonblank; otherwise NewEngine returns an error matching ErrInvalidConfig.
|
|
||||||
// Repeating this option replaces the earlier valid fallback source.
|
|
||||||
//
|
|
||||||
// This option controls profile-definition lookup, not provider or generation
|
|
||||||
// failover.
|
|
||||||
func WithFallbackProfileFS(fsys fs.FS, root string) Option {
|
|
||||||
return optionFunc(func(options *engineOptions) error {
|
|
||||||
return options.internal.SetFallbackProfileFS(fsys, root)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithProfiles configures in-memory profiles that take precedence over
|
|
||||||
// ordinary configured, application fallback, and built-in profiles.
|
|
||||||
//
|
|
||||||
// NewEngine locally validates and copies every profile. IDs must be unique
|
|
||||||
// within one call. An invalid local definition, duplicate ID, or unsupported
|
|
||||||
// ExtraParams value makes construction fail with ErrInvalidConfig. A derived
|
|
||||||
// profile's base reference and resolved target completeness are checked when it
|
|
||||||
// is selected or inspected. Repeating WithProfiles replaces the complete
|
|
||||||
// earlier in-memory set rather than merging it.
|
|
||||||
func WithProfiles(profiles ...Profile) Option {
|
|
||||||
return optionFunc(func(options *engineOptions) error {
|
|
||||||
definitions := make([]domain.ExecutionProfile, len(profiles))
|
|
||||||
for index, publicProfile := range profiles {
|
|
||||||
definitions[index] = toDomainProfile(publicProfile)
|
|
||||||
}
|
|
||||||
return options.internal.SetMemoryProfiles(definitions)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSchemaFS loads JSON Schema documents from fsys under root.
|
// WithSchemaFS loads JSON Schema documents from fsys under root.
|
||||||
//
|
//
|
||||||
// Prompt schema_path values resolve within this source when schema validation
|
// Prompt schema_path values resolve within this source when schema validation
|
||||||
@@ -302,14 +222,17 @@ func WithSchemaFile(path string) Option {
|
|||||||
|
|
||||||
// NewEngine constructs an Engine from configuration and options.
|
// NewEngine constructs an Engine from configuration and options.
|
||||||
//
|
//
|
||||||
// Options are applied in order according to [Option]. PromptDir is required
|
// ConfigKit composes caller-selected underlays in their given order, followed
|
||||||
// unless a prompt-source option is present. Construction validates option
|
// by the optional main input, after the default-on bundled OpenRouter underlay.
|
||||||
// arguments, in-memory profiles, and backend registrations but defers reading
|
// The bundle adds a root backend and profiles to inventories, but no prompts
|
||||||
// and validating prompt, file-backed profile, and schema contents until Prepare
|
// or implicit profile selection. Construction validates the final effective
|
||||||
// or Run needs them.
|
// backend, profile, and prompt definitions and captures referenced prompt
|
||||||
|
// content. A missing prompt collection is allowed. Schema documents and
|
||||||
|
// request artifacts remain runtime resources.
|
||||||
//
|
//
|
||||||
// NewEngine returns an error matching ErrInvalidConfig for invalid
|
// NewEngine returns an error matching ErrInvalidConfig for invalid
|
||||||
// configuration, options, or backend-capacity policies. Each constructed
|
// configuration or options, preserving a ConfigKit LoadError when loading
|
||||||
|
// fails. Each constructed
|
||||||
// Engine has independent backend-capacity pools. Construction does not perform
|
// Engine has independent backend-capacity pools. Construction does not perform
|
||||||
// model requests or require credentials. A negative Config.TransientRetries
|
// model requests or require credentials. A negative Config.TransientRetries
|
||||||
// is invalid.
|
// is invalid.
|
||||||
@@ -320,28 +243,27 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := opt.apply(&options); err != nil {
|
if err := opt.apply(&options); err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err)
|
return nil, fmt.Errorf("%w: %w", ErrInvalidConfig, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assembled, err := internalengine.New(
|
assembled, err := internalengine.New(
|
||||||
internalengine.Config{PromptDir: cfg.PromptDir, ProfileDir: cfg.ProfileDir, SchemaDir: cfg.SchemaDir, Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient, TransientRetries: copyIntPtr(cfg.TransientRetries)},
|
internalengine.Config{Sources: internalengine.ConfigurationSources{Underlays: cfg.Sources.Underlays, Main: cfg.Sources.Main}, SchemaDir: cfg.SchemaDir, Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient, TransientRetries: copyIntPtr(cfg.TransientRetries)},
|
||||||
options.internal,
|
options.internal,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err)
|
return nil, fmt.Errorf("%w: %w", ErrInvalidConfig, err)
|
||||||
}
|
}
|
||||||
return &Engine{internal: assembled}, nil
|
return &Engine{internal: assembled}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InspectPrompt resolves one explicit prompt definition without selecting a
|
// InspectPrompt resolves one prompt ID without selecting a profile or starting
|
||||||
// profile or starting execution work.
|
// execution work.
|
||||||
//
|
//
|
||||||
// InspectPrompt requires a nonblank promptID. It passes nonblank promptID and
|
// InspectPrompt requires a nonblank promptID and performs an exact,
|
||||||
// promptVersion values unchanged to the engine's ordinary, case-sensitive
|
// case-sensitive lookup in the captured configuration snapshot. A
|
||||||
// prompt selection. An empty version succeeds only when that source has one
|
// namespace-scoped view qualifies slashless caller IDs before lookup; already
|
||||||
// selected ID; a nonempty version selects one exact ID/version pair. The
|
// qualified IDs pass through.
|
||||||
// configured prompt source is used without merging, fallback, or enumeration.
|
|
||||||
//
|
//
|
||||||
// A successful result proves that the selected definition and any referenced
|
// A successful result proves that the selected definition and any referenced
|
||||||
// message content files were structurally loaded. Inputs are returned in
|
// message content files were structurally loaded. Inputs are returned in
|
||||||
@@ -349,32 +271,30 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
|
|||||||
// resolved. OutputContract is the normalized declared contract, with a JSON
|
// resolved. OutputContract is the normalized declared contract, with a JSON
|
||||||
// Schema path when declared but without loading or compiling that schema.
|
// Schema path when declared but without loading or compiling that schema.
|
||||||
// PromptHash is the same opaque equality value as PreparedRun.PromptHash for
|
// PromptHash is the same opaque equality value as PreparedRun.PromptHash for
|
||||||
// the selected definition and observed source state; its spelling, length,
|
// the selected captured definition; its spelling, length,
|
||||||
// encoding, algorithm, and security properties are not contracts.
|
// encoding, algorithm, and security properties are not contracts.
|
||||||
//
|
//
|
||||||
// This method does not return prompt bodies, templates, source paths, schemas,
|
// This method does not return prompt bodies, templates, source paths, schemas,
|
||||||
// rendered messages, or execution settings. It does not resolve a profile or
|
// rendered messages, or execution settings. It does not resolve a profile or
|
||||||
// credential, read artifacts or schemas, render, validate, admit capacity,
|
// credential, read artifacts or schemas, render, validate, admit capacity,
|
||||||
// contact a provider, or generate model output. The returned PromptInspection
|
// contact a provider, or generate model output. The returned PromptInspection
|
||||||
// and its input slice are caller-owned. Filesystem-backed inspection is a
|
// and its input slice are caller-owned. Inspection does not reload configuration
|
||||||
// point-in-time lookup and does not freeze a definition for later execution.
|
// or content files; construct another Engine to observe source changes.
|
||||||
//
|
//
|
||||||
// A nil Engine returns an error matching ErrInvalidConfig. A blank prompt ID
|
// A nil Engine returns an error matching ErrInvalidConfig. A blank prompt ID
|
||||||
// matches ErrInvalidRequest. An absent exact ID or version matches
|
// matches ErrInvalidRequest. An absent exact ID matches ErrPromptNotFound and
|
||||||
// ErrPromptNotFound and not ErrPromptLoad. Malformed, unreadable, duplicate,
|
// not ErrPromptLoad. Invalid definitions and resource failures reject
|
||||||
// ambiguous, referenced-content, or hashing failures match ErrPromptLoad.
|
// construction instead. Cancellation during lookup preserves the context
|
||||||
// Cancellation during lookup matches ErrPromptLoad while preserving the
|
// error. InspectPrompt returns no partial result on error.
|
||||||
// context error. InspectPrompt returns no partial result on error.
|
|
||||||
func (e *Engine) InspectPrompt(
|
func (e *Engine) InspectPrompt(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
promptID string,
|
promptID string,
|
||||||
promptVersion string,
|
|
||||||
) (*PromptInspection, error) {
|
) (*PromptInspection, error) {
|
||||||
if e == nil || e.internal == nil {
|
if e == nil || e.internal == nil {
|
||||||
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
inspection, err := e.internal.InspectPrompt(ctx, promptID, promptVersion)
|
inspection, err := e.internal.InspectPrompt(ctx, promptID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, mapPublicError(err)
|
return nil, mapPublicError(err)
|
||||||
}
|
}
|
||||||
@@ -384,10 +304,9 @@ func (e *Engine) InspectPrompt(
|
|||||||
// InspectProfile resolves one explicit profile without selecting a prompt or
|
// InspectProfile resolves one explicit profile without selecting a prompt or
|
||||||
// starting execution work.
|
// starting execution work.
|
||||||
//
|
//
|
||||||
// InspectProfile trims surrounding whitespace from profileID and looks up the
|
// InspectProfile looks up a nonblank profileID exactly and case-sensitively
|
||||||
// resulting nonblank ID exactly and case-sensitively through the engine's
|
// in the captured effective configuration. It applies the framework timeout
|
||||||
// in-memory, ordinary configured-source, application fallback, and built-in
|
// baseline, selected
|
||||||
// profile precedence. It applies the framework timeout baseline, selected
|
|
||||||
// backend, and then selected profile to EffectiveModelParams without a request
|
// backend, and then selected profile to EffectiveModelParams without a request
|
||||||
// override. BackendID is empty for an endpoint-only profile.
|
// override. BackendID is empty for an endpoint-only profile.
|
||||||
//
|
//
|
||||||
@@ -398,16 +317,15 @@ func (e *Engine) InspectPrompt(
|
|||||||
// absent or blank named environment variable is not an error.
|
// absent or blank named environment variable is not an error.
|
||||||
//
|
//
|
||||||
// The returned ProfileInspection and all nested mutable values are
|
// The returned ProfileInspection and all nested mutable values are
|
||||||
// caller-owned. Filesystem-backed inspection is a point-in-time lookup and
|
// caller-owned. Inspection does not reload configuration; construct another
|
||||||
// does not freeze the profile for a later execution. This method does not load
|
// Engine to observe source changes. This method does not load
|
||||||
// a prompt, render, read artifacts or schemas, admit backend capacity, contact
|
// a prompt, render, read artifacts or schemas, admit backend capacity, contact
|
||||||
// a provider, or generate model output.
|
// a provider, or generate model output.
|
||||||
//
|
//
|
||||||
// A nil Engine returns an error matching ErrInvalidConfig. A blank profile ID
|
// A nil Engine returns an error matching ErrInvalidConfig. A blank profile ID
|
||||||
// matches ErrInvalidRequest. An absent exact ID matches ErrProfileNotFound and
|
// matches ErrInvalidRequest. An absent exact ID matches ErrProfileNotFound and
|
||||||
// not ErrProfileLoad. Malformed or unreadable profile data, an unknown backend,
|
// not ErrProfileLoad. Invalid definitions or unknown backends reject
|
||||||
// or an invalid resolved target matches ErrProfileLoad. Cancellation during
|
// construction instead. Cancellation preserves the context error.
|
||||||
// profile loading matches ErrProfileLoad while preserving the context error.
|
|
||||||
// InspectProfile returns no partial result on error.
|
// InspectProfile returns no partial result on error.
|
||||||
func (e *Engine) InspectProfile(ctx context.Context, profileID string) (*ProfileInspection, error) {
|
func (e *Engine) InspectProfile(ctx context.Context, profileID string) (*ProfileInspection, error) {
|
||||||
if e == nil || e.internal == nil {
|
if e == nil || e.internal == nil {
|
||||||
@@ -441,6 +359,32 @@ func (e *Engine) InspectProfile(ctx context.Context, profileID string) (*Profile
|
|||||||
// relationship to ctx.Err is promised. Prepare returns no partial result on
|
// relationship to ctx.Err is promised. Prepare returns no partial result on
|
||||||
// error. It validates RunRequest.TransientRetries but makes no model attempts.
|
// error. It validates RunRequest.TransientRetries but makes no model attempts.
|
||||||
func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, error) {
|
func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, error) {
|
||||||
|
prepared, err := e.prepareRequest(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fromDomainPreparedRun(prepared), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRequest reports whether req passes the same preparation path used by
|
||||||
|
// [Engine.Prepare] at the time of this call. A nil error means this concrete
|
||||||
|
// request passed preparation. It checks only this invocation and returns errors
|
||||||
|
// as encountered rather than aggregating them. All supplied inputs follow the
|
||||||
|
// same loading behavior as [Engine.Prepare], including inputs unused by the
|
||||||
|
// selected template branch. The method does not call an LLM or retain a
|
||||||
|
// prepared snapshot or executable handle, but preparation may still perform
|
||||||
|
// resource I/O and incur its associated time and memory costs.
|
||||||
|
//
|
||||||
|
// Its errors and context handling follow [Engine.Prepare], including an error
|
||||||
|
// matching ErrInvalidConfig when the Engine is nil. Success does not guarantee
|
||||||
|
// provider acceptance or later execution success. Use [Engine.PrepareExecution]
|
||||||
|
// when execution must use the snapshot produced by preparation.
|
||||||
|
func (e *Engine) ValidateRequest(ctx context.Context, req RunRequest) error {
|
||||||
|
_, err := e.prepareRequest(ctx, req)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) prepareRequest(ctx context.Context, req RunRequest) (*domain.PreparedRun, error) {
|
||||||
ctx = observation.WithoutExecutionState(ctx)
|
ctx = observation.WithoutExecutionState(ctx)
|
||||||
if e == nil || e.internal == nil {
|
if e == nil || e.internal == nil {
|
||||||
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
||||||
@@ -455,15 +399,16 @@ func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, mapPublicError(err)
|
return nil, mapPublicError(err)
|
||||||
}
|
}
|
||||||
return fromDomainPreparedRun(prepared), nil
|
return prepared, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareExecution completely prepares a prompt request without calling the
|
// PrepareExecution completely prepares a prompt request without calling the
|
||||||
// configured LLMClient or reserving backend admission capacity. Validated
|
// configured LLMClient or reserving backend admission capacity. Validated
|
||||||
// RunRequest.AppendedMessages are included in the frozen effective messages.
|
// RunRequest.AppendedMessages are included in the frozen effective messages.
|
||||||
//
|
//
|
||||||
// The returned opaque handle is bound to this Engine and permits one
|
// The returned opaque handle is bound to this Engine's underlying execution
|
||||||
// [Engine.RunPrepared] invocation. Preparation freezes the selected sources,
|
// owner, shared with its namespace views, and permits one [Engine.RunPrepared]
|
||||||
|
// invocation in total. Preparation freezes the selected sources,
|
||||||
// rendered messages, effective settings, inputs, provider structured-output
|
// rendered messages, effective settings, inputs, provider structured-output
|
||||||
// metadata, and validation resources needed by that invocation. The handle
|
// metadata, and validation resources needed by that invocation. The handle
|
||||||
// retains a direct RunRequest.APIKey only in private execution state;
|
// retains a direct RunRequest.APIKey only in private execution state;
|
||||||
@@ -495,8 +440,9 @@ func (e *Engine) PrepareExecution(ctx context.Context, req RunRequest) (*Prepare
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run prepares a request, invokes the configured LLMClient, and validates the
|
// Run prepares a request, invokes the configured LLMClient, and validates the
|
||||||
// generated output. Each call resolves current sources and composes a fresh,
|
// generated output. Each call uses the Engine's captured configuration and
|
||||||
// stateless effective prompt with any validated RunRequest.AppendedMessages.
|
// composes a fresh effective prompt with any validated
|
||||||
|
// RunRequest.AppendedMessages.
|
||||||
//
|
//
|
||||||
// A content-validation failure is a successful run whose
|
// A content-validation failure is a successful run whose
|
||||||
// RunResult.Validation has Status ValidationFailed. When its output contract
|
// RunResult.Validation has Status ValidationFailed. When its output contract
|
||||||
@@ -574,6 +520,11 @@ func (e *Engine) run(ctx context.Context, req RunRequest) (*RunResult, error) {
|
|||||||
// RunPrepared atomically claims and executes a handle created by
|
// RunPrepared atomically claims and executes a handle created by
|
||||||
// [Engine.PrepareExecution].
|
// [Engine.PrepareExecution].
|
||||||
//
|
//
|
||||||
|
// The originating Engine and namespace views sharing its underlying owner may
|
||||||
|
// claim the handle. Execution uses its captured selections without applying the
|
||||||
|
// invoking view's namespaces. An Engine constructed separately by [NewEngine]
|
||||||
|
// is a foreign owner.
|
||||||
|
//
|
||||||
// A valid owning-Engine invocation consumes the handle's one attempt before
|
// A valid owning-Engine invocation consumes the handle's one attempt before
|
||||||
// credential revalidation, backend admission, generation, or validation.
|
// credential revalidation, backend admission, generation, or validation.
|
||||||
// Cancellation, capacity rejection, generation failure, operational
|
// Cancellation, capacity rejection, generation failure, operational
|
||||||
|
|||||||
3705
engine_test.go
3705
engine_test.go
File diff suppressed because it is too large
Load Diff
22
errors.go
22
errors.go
@@ -5,12 +5,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/capacity"
|
"go.maximumdirect.net/promptkit/internal/capacity"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/generation"
|
"go.maximumdirect.net/promptkit/internal/generation"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/llm"
|
"go.maximumdirect.net/promptkit/internal/llm"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/profile"
|
"go.maximumdirect.net/promptkit/internal/profile"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/promptdef"
|
"go.maximumdirect.net/promptkit/internal/promptdef"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/usecase"
|
"go.maximumdirect.net/promptkit/internal/usecase"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mapPublicError(err error) error {
|
func mapPublicError(err error) error {
|
||||||
@@ -72,10 +72,6 @@ func publicErrorFor(err error) error {
|
|||||||
return ErrPromptLoad
|
return ErrPromptLoad
|
||||||
case errors.Is(err, usecase.ErrProfileLoad):
|
case errors.Is(err, usecase.ErrProfileLoad):
|
||||||
return ErrProfileLoad
|
return ErrProfileLoad
|
||||||
case errors.Is(err, promptdef.ErrInvalidYAML), errors.Is(err, promptdef.ErrInvalidPromptDefinition):
|
|
||||||
return ErrPromptLoad
|
|
||||||
case isProfileLoadCause(err):
|
|
||||||
return ErrProfileLoad
|
|
||||||
case errors.Is(err, usecase.ErrAPIKeyEnvMissing):
|
case errors.Is(err, usecase.ErrAPIKeyEnvMissing):
|
||||||
return errors.Join(ErrInvalidRequest, ErrAPIKeyEnvMissing)
|
return errors.Join(ErrInvalidRequest, ErrAPIKeyEnvMissing)
|
||||||
case errors.Is(err, capacity.ErrCapacityExceeded):
|
case errors.Is(err, capacity.ErrCapacityExceeded):
|
||||||
@@ -94,9 +90,3 @@ func publicErrorFor(err error) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isProfileLoadCause(err error) bool {
|
|
||||||
return errors.Is(err, profile.ErrInvalidYAML) ||
|
|
||||||
errors.Is(err, profile.ErrInvalidProfile) ||
|
|
||||||
errors.Is(err, profile.ErrRawAPIKeyNotAllowed)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/llm"
|
"go.maximumdirect.net/promptkit/internal/llm"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/usecase"
|
"go.maximumdirect.net/promptkit/internal/usecase"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMapPublicErrorPreservesGenerationCancellation(t *testing.T) {
|
func TestMapPublicErrorPreservesGenerationCancellation(t *testing.T) {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit"
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed prompts/*.yaml schemas/*.json
|
//go:embed prompts/*.yaml schemas/*.json
|
||||||
@@ -78,14 +79,26 @@ type summary struct {
|
|||||||
|
|
||||||
func run() (summary, error) {
|
func run() (summary, error) {
|
||||||
transport := &scriptedTransport{}
|
transport := &scriptedTransport{}
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{HTTPClient: &http.Client{Transport: transport}},
|
inputs, err := promptkit.FilesystemInputs("example prompts", assets, "prompts")
|
||||||
promptkit.WithPromptFS(assets, "prompts"),
|
if err != nil {
|
||||||
promptkit.WithSchemaFS(assets, "schemas"),
|
return summary{}, err
|
||||||
promptkit.WithBackend(promptkit.Backend{ID: "offline-backend", Endpoint: "https://example.invalid/v1", ConcurrencyLimit: 1}),
|
}
|
||||||
promptkit.WithProfiles(
|
backendID, proposalModel, assessmentModel := "offline-backend", "proposal-model", "assessment-model"
|
||||||
promptkit.Profile{ID: "proposal-profile", BackendID: "offline-backend", Model: "proposal-model"},
|
backend := promptkit.LocalBackend("https://example.invalid/v1", 1)
|
||||||
promptkit.Profile{ID: "assessment-profile", BackendID: "offline-backend", Model: "assessment-model"},
|
backend.ID = backendID
|
||||||
))
|
inputs = append(inputs,
|
||||||
|
configkit.Input{Source: promptkit.BackendSource("example backend", []promptkit.BackendConfig{
|
||||||
|
backend,
|
||||||
|
})},
|
||||||
|
configkit.Input{Source: promptkit.ProfileSource("example profiles", []promptkit.ProfileConfig{
|
||||||
|
{ID: "proposal-profile", Backend: &backendID, Model: &proposalModel},
|
||||||
|
{ID: "assessment-profile", Backend: &backendID, Model: &assessmentModel},
|
||||||
|
})},
|
||||||
|
)
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{
|
||||||
|
Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: inputs}}},
|
||||||
|
HTTPClient: &http.Client{Transport: transport},
|
||||||
|
}, promptkit.WithBundledOpenRouter(false), promptkit.WithSchemaFS(assets, "schemas")) // Keep the example's configuration self-contained.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return summary{}, err
|
return summary{}, err
|
||||||
}
|
}
|
||||||
@@ -145,13 +158,16 @@ func run() (summary, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
value, err := run()
|
if err := execute(os.Stdout); err != nil {
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if err := json.NewEncoder(os.Stdout).Encode(value); err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func execute(out io.Writer) error {
|
||||||
|
value, err := run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.NewEncoder(out).Encode(value)
|
||||||
|
}
|
||||||
|
|||||||
30
examples/go-library/observe/main_test.go
Normal file
30
examples/go-library/observe/main_test.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestObserveExample(t *testing.T) {
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := execute(&output); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var got struct {
|
||||||
|
ProposalAttempts *int `json:"proposal_attempts"`
|
||||||
|
ProposalRetries *int `json:"proposal_transient_retries"`
|
||||||
|
UsageComplete *bool `json:"proposal_usage_complete"`
|
||||||
|
AssessmentAttempts *int `json:"assessment_attempts"`
|
||||||
|
AssessmentRetries *int `json:"assessment_transient_retries"`
|
||||||
|
Outcome string `json:"assessment_outcome"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(output.Bytes(), &got); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.ProposalAttempts == nil || *got.ProposalAttempts != 2 || got.ProposalRetries == nil || *got.ProposalRetries != 1 ||
|
||||||
|
got.UsageComplete == nil || *got.UsageComplete || got.AssessmentAttempts == nil || *got.AssessmentAttempts != 1 ||
|
||||||
|
got.AssessmentRetries == nil || *got.AssessmentRetries != 0 || got.Outcome != "accepted" {
|
||||||
|
t.Fatalf("unexpected observation summary: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
id: example.assessment
|
prompts:
|
||||||
version: "1.0.0"
|
- id: example.assessment
|
||||||
default_profile: assessment-profile
|
default_profile: assessment-profile
|
||||||
description: Assess a structured proposal independently of schema validity.
|
description: Assess a structured proposal independently of schema validity.
|
||||||
inputs:
|
inputs:
|
||||||
- name: proposal
|
- name: proposal
|
||||||
required: true
|
required: true
|
||||||
content_type: application/json
|
content_type: application/json
|
||||||
messages:
|
messages:
|
||||||
- role: system
|
- role: system
|
||||||
content: Return a concise assessment as JSON.
|
content: Return a concise assessment as JSON.
|
||||||
- role: user
|
- role: user
|
||||||
content: '{{input "proposal"}}'
|
content: '{{input "proposal"}}'
|
||||||
output:
|
output:
|
||||||
format: json
|
format: json
|
||||||
validation_mode: json_schema
|
validation_mode: json_schema
|
||||||
schema_path: assessment.schema.json
|
schema_path: assessment.schema.json
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
id: example.proposal
|
prompts:
|
||||||
version: "1.0.0"
|
- id: example.proposal
|
||||||
default_profile: proposal-profile
|
default_profile: proposal-profile
|
||||||
description: Propose a bounded improvement from a synthetic idea.
|
description: Propose a bounded improvement from a synthetic idea.
|
||||||
inputs:
|
inputs:
|
||||||
- name: idea
|
- name: idea
|
||||||
required: true
|
required: true
|
||||||
content_type: text/plain
|
content_type: text/plain
|
||||||
messages:
|
messages:
|
||||||
- role: system
|
- role: system
|
||||||
content: Return a concise proposal as JSON.
|
content: Return a concise proposal as JSON.
|
||||||
- role: user
|
- role: user
|
||||||
content: '{{input "idea"}}'
|
content: '{{input "idea"}}'
|
||||||
output:
|
output:
|
||||||
format: json
|
format: json
|
||||||
validation_mode: json_schema
|
validation_mode: json_schema
|
||||||
schema_path: proposal.schema.json
|
schema_path: proposal.schema.json
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
backends:
|
||||||
|
- id: offline
|
||||||
|
endpoint: https://review.example.invalid/v1
|
||||||
|
profiles:
|
||||||
|
- id: audit
|
||||||
|
backend: review/offline
|
||||||
|
model: review-model
|
||||||
|
prompts:
|
||||||
|
- id: check
|
||||||
|
default_profile: review/audit
|
||||||
|
description: Inspect a review item without contacting a model provider.
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: Review the supplied synthetic example.
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: "2"
|
||||||
|
config:
|
||||||
|
imports:
|
||||||
|
- path: fragments/resources.yaml
|
||||||
|
required: true
|
||||||
|
- path: fragments/prepare.yaml
|
||||||
|
destination: [prompts]
|
||||||
|
placement: sequence-item
|
||||||
|
required: true
|
||||||
|
release_version: "1.4.0"
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
id: example.prepare
|
id: prepare
|
||||||
version: "1.0.0"
|
default_profile: support/fast
|
||||||
default_profile: offline-example
|
|
||||||
description: Prepare a prompt without contacting a model provider.
|
description: Prepare a prompt without contacting a model provider.
|
||||||
inputs:
|
inputs:
|
||||||
- name: note
|
- name: note
|
||||||
@@ -10,7 +9,7 @@ messages:
|
|||||||
- role: system
|
- role: system
|
||||||
content: Summarize the note in one sentence.
|
content: Summarize the note in one sentence.
|
||||||
- role: user
|
- role: user
|
||||||
content: '{{input "note"}}'
|
content_file: ../messages/note.md
|
||||||
output:
|
output:
|
||||||
format: text
|
format: text
|
||||||
validation_mode: basic
|
validation_mode: basic
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
backends:
|
||||||
|
- id: offline
|
||||||
|
endpoint: https://example.invalid/v1
|
||||||
|
profiles:
|
||||||
|
- id: base
|
||||||
|
backend: support/offline
|
||||||
|
model: offline-model
|
||||||
|
timeout_seconds: 90
|
||||||
|
- id: fast
|
||||||
|
extends: {id: base}
|
||||||
|
timeout_seconds: 60
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{{input "note"}}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# A later input replaces the profile with an inherited variant.
|
||||||
|
profiles:
|
||||||
|
- id: fast
|
||||||
|
extends: {id: base}
|
||||||
|
timeout_seconds: 30
|
||||||
@@ -4,57 +4,162 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit"
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type summary struct {
|
type summary struct {
|
||||||
PromptID string `json:"prompt_id"`
|
PromptID string `json:"prompt_id"`
|
||||||
PromptVersion string `json:"prompt_version"`
|
SelectedProfile string `json:"selected_profile"`
|
||||||
SelectedProfile string `json:"selected_profile"`
|
Model string `json:"model"`
|
||||||
Model string `json:"model"`
|
MessageCount int `json:"message_count"`
|
||||||
MessageCount int `json:"message_count"`
|
ProfileTimeout int `json:"profile_timeout_seconds"`
|
||||||
|
Namespaces []string `json:"namespaces"`
|
||||||
|
Collections []collectionSummary `json:"collections"`
|
||||||
|
ScopedPrompts []string `json:"scoped_prompts"`
|
||||||
|
ReviewPrompts []string `json:"review_prompts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type collectionSummary struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
ReleaseVersion *string `json:"release_version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
engine, err := promptkit.NewEngine(
|
if err := run(context.Background(), "examples/go-library/prepare", os.Stdout); err != nil {
|
||||||
promptkit.Config{},
|
fmt.Fprintln(os.Stderr, err)
|
||||||
promptkit.WithPromptFile("examples/go-library/prepare/prompt.yaml"),
|
os.Exit(1)
|
||||||
promptkit.WithProfiles(promptkit.Profile{
|
}
|
||||||
ID: "offline-example",
|
}
|
||||||
Endpoint: "https://example.invalid/v1",
|
|
||||||
Model: "offline-model",
|
func run(ctx context.Context, exampleDir string, out io.Writer) error {
|
||||||
}),
|
support, err := promptkit.CollectionInput(
|
||||||
|
"support",
|
||||||
|
configkit.File(filepath.Join(exampleDir, "collections", "support", "collection.yaml")),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exit(err)
|
return err
|
||||||
|
}
|
||||||
|
review, err := promptkit.CollectionInput(
|
||||||
|
"review",
|
||||||
|
configkit.File(filepath.Join(exampleDir, "collections", "review", "collection.yaml")),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
override, err := promptkit.CollectionInput(
|
||||||
|
"support",
|
||||||
|
configkit.File(filepath.Join(exampleDir, "collections", "support", "operator-overrides.yaml")),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{
|
||||||
|
{Inputs: []configkit.Input{support, review}},
|
||||||
|
{Inputs: []configkit.Input{override}},
|
||||||
|
},
|
||||||
|
}}, promptkit.WithBundledOpenRouter(false)) // Keep this example's inventory caller-owned.
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
scoped, err := engine.WithNamespaces(promptkit.Namespaces{
|
||||||
|
Prompt: "support", Profile: "support", Backend: "support",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
view, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, found := view.Explain(configkit.Keys("collections", "support", "profiles").Index(1).Key("timeout_seconds")); !found {
|
||||||
|
return fmt.Errorf("collection profile override was not found")
|
||||||
|
}
|
||||||
|
requirements, err := scoped.InspectRequirements(ctx, promptkit.RequirementInspectionRequest{
|
||||||
|
PromptID: "prepare",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !requirements.Complete || len(requirements.Inputs) != 1 ||
|
||||||
|
requirements.Inputs[0].Name != "note" ||
|
||||||
|
requirements.Inputs[0].Kind != promptkit.RequirementRequired {
|
||||||
|
return fmt.Errorf("required note input was not discovered before preparation")
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
|
request := promptkit.RunRequest{
|
||||||
PromptID: "example.prepare",
|
PromptID: "prepare",
|
||||||
|
ProfileID: "fast",
|
||||||
Inputs: map[string]promptkit.ArtifactRef{
|
Inputs: map[string]promptkit.ArtifactRef{
|
||||||
"note": promptkit.Inline("Ada finished the migration review."),
|
"note": promptkit.Inline("Ada finished the migration review."),
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
if err := scoped.ValidateRequest(ctx, request); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
prepared, err := scoped.Prepare(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
profile, err := scoped.InspectProfile(ctx, "fast")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if profile.EffectiveModelParams.TimeoutSeconds != 30 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"later profile override was not applied: timeout is %d seconds",
|
||||||
|
profile.EffectiveModelParams.TimeoutSeconds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
namespaces, err := scoped.ListNamespaces(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collections, err := scoped.ListCollections(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
scopedPrompts, err := scoped.ListPrompts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reviewPrompts, err := scoped.ListPrompts(ctx, promptkit.NamespaceFilter{
|
||||||
|
Mode: promptkit.NamespaceNamed, Name: "review",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exit(err)
|
return err
|
||||||
|
}
|
||||||
|
scopedPromptIDs := make([]string, len(scopedPrompts))
|
||||||
|
for index := range scopedPrompts {
|
||||||
|
scopedPromptIDs[index] = scopedPrompts[index].ID
|
||||||
|
}
|
||||||
|
reviewPromptIDs := make([]string, len(reviewPrompts))
|
||||||
|
for index := range reviewPrompts {
|
||||||
|
reviewPromptIDs[index] = reviewPrompts[index].ID
|
||||||
|
}
|
||||||
|
collectionSummaries := make([]collectionSummary, len(collections))
|
||||||
|
for index := range collections {
|
||||||
|
collectionSummaries[index] = collectionSummary{
|
||||||
|
Namespace: collections[index].Namespace, ReleaseVersion: collections[index].ReleaseVersion,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder := json.NewEncoder(os.Stdout)
|
encoder := json.NewEncoder(out)
|
||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
if err := encoder.Encode(summary{
|
return encoder.Encode(summary{
|
||||||
PromptID: prepared.PromptID,
|
PromptID: prepared.PromptID,
|
||||||
PromptVersion: prepared.PromptVersion,
|
|
||||||
SelectedProfile: prepared.SelectedProfileID,
|
SelectedProfile: prepared.SelectedProfileID,
|
||||||
Model: prepared.EffectiveModelParams.Model,
|
Model: prepared.EffectiveModelParams.Model,
|
||||||
MessageCount: len(prepared.Messages),
|
MessageCount: len(prepared.Messages),
|
||||||
}); err != nil {
|
ProfileTimeout: profile.EffectiveModelParams.TimeoutSeconds,
|
||||||
exit(err)
|
Namespaces: namespaces,
|
||||||
}
|
Collections: collectionSummaries,
|
||||||
}
|
ScopedPrompts: scopedPromptIDs,
|
||||||
|
ReviewPrompts: reviewPromptIDs,
|
||||||
func exit(err error) {
|
})
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|||||||
45
examples/go-library/prepare/main_test.go
Normal file
45
examples/go-library/prepare/main_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrepareExample(t *testing.T) {
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := run(t.Context(), ".", &output); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var got struct {
|
||||||
|
PromptID string `json:"prompt_id"`
|
||||||
|
Profile string `json:"selected_profile"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
MessageCount int `json:"message_count"`
|
||||||
|
Timeout int `json:"profile_timeout_seconds"`
|
||||||
|
Namespaces []string `json:"namespaces"`
|
||||||
|
Collections []map[string]json.RawMessage `json:"collections"`
|
||||||
|
ScopedPrompts []string `json:"scoped_prompts"`
|
||||||
|
ReviewPrompts []string `json:"review_prompts"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(output.Bytes(), &got); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.PromptID != "support/prepare" || got.Profile != "support/fast" || got.Model != "offline-model" ||
|
||||||
|
got.MessageCount <= 0 || got.Timeout != 30 || !reflect.DeepEqual(got.Namespaces, []string{"review", "support"}) ||
|
||||||
|
!reflect.DeepEqual(got.ScopedPrompts, []string{"support/prepare"}) || !reflect.DeepEqual(got.ReviewPrompts, []string{"review/check"}) || len(got.Collections) != 2 {
|
||||||
|
t.Fatalf("unexpected preparation summary: %+v", got)
|
||||||
|
}
|
||||||
|
for index, want := range []struct{ namespace, release string }{{"review", ""}, {"support", "1.4.0"}} {
|
||||||
|
var namespace string
|
||||||
|
if err := json.Unmarshal(got.Collections[index]["namespace"], &namespace); err != nil || namespace != want.namespace {
|
||||||
|
t.Fatalf("collection namespace = %q, %v", namespace, err)
|
||||||
|
}
|
||||||
|
value, present := got.Collections[index]["release_version"]
|
||||||
|
var release *string
|
||||||
|
if !present || json.Unmarshal(value, &release) != nil || (index == 0 && release != nil) || (index == 1 && (release == nil || *release != want.release)) {
|
||||||
|
t.Fatalf("collection release = %s, present %t; want %q", value, present, want.release)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit"
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type deterministicClient struct{}
|
type deterministicClient struct{}
|
||||||
@@ -38,44 +41,41 @@ type summary struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
engine, err := promptkit.NewEngine(
|
if err := run(context.Background(), "examples/go-library/run", os.Stdout); err != nil {
|
||||||
promptkit.Config{},
|
fmt.Fprintln(os.Stderr, err)
|
||||||
promptkit.WithPromptFile("examples/go-library/run/prompt.yaml"),
|
os.Exit(1)
|
||||||
promptkit.WithProfiles(promptkit.Profile{
|
}
|
||||||
ID: "offline-example",
|
}
|
||||||
Endpoint: "https://example.invalid/v1",
|
|
||||||
Model: "offline-model",
|
func run(ctx context.Context, exampleDir string, out io.Writer) error {
|
||||||
}),
|
endpoint, model := "https://example.invalid/v1", "offline-model"
|
||||||
promptkit.WithLLMClient(deterministicClient{}),
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
)
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: promptkit.ProfileSource("example profile", []promptkit.ProfileConfig{
|
||||||
|
{ID: "offline-example", Endpoint: &endpoint, Model: &model},
|
||||||
|
})}}}},
|
||||||
|
Main: &configkit.Input{Source: configkit.File(filepath.Join(exampleDir, "prompt.yaml"))},
|
||||||
|
}}, promptkit.WithBundledOpenRouter(false), promptkit.WithLLMClient(deterministicClient{})) // Use only this example's offline configuration.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exit(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := engine.Run(context.Background(), promptkit.RunRequest{
|
result, err := engine.Run(ctx, promptkit.RunRequest{
|
||||||
PromptID: "example.run",
|
PromptID: "example.run",
|
||||||
Inputs: map[string]promptkit.ArtifactRef{
|
Inputs: map[string]promptkit.ArtifactRef{
|
||||||
"note": promptkit.Inline("Ada finished the migration review."),
|
"note": promptkit.Inline("Ada finished the migration review."),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exit(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder := json.NewEncoder(os.Stdout)
|
encoder := json.NewEncoder(out)
|
||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
if err := encoder.Encode(summary{
|
return encoder.Encode(summary{
|
||||||
Output: result.RawOutput,
|
Output: result.RawOutput,
|
||||||
ValidationStatus: result.Validation.Status,
|
ValidationStatus: result.Validation.Status,
|
||||||
IsValid: result.Validation.IsValid,
|
IsValid: result.Validation.IsValid,
|
||||||
Model: result.ModelName,
|
Model: result.ModelName,
|
||||||
TotalTokens: result.Usage.TotalTokens,
|
TotalTokens: result.Usage.TotalTokens,
|
||||||
}); err != nil {
|
})
|
||||||
exit(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func exit(err error) {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|||||||
28
examples/go-library/run/main_test.go
Normal file
28
examples/go-library/run/main_test.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunExample(t *testing.T) {
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := run(t.Context(), ".", &output); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var got struct {
|
||||||
|
Output string `json:"output"`
|
||||||
|
Valid *bool `json:"is_valid"`
|
||||||
|
Status string `json:"validation_status"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Tokens *int `json:"total_tokens"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(output.Bytes(), &got); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.Output != "Ada finished the migration review." || got.Valid == nil || !*got.Valid ||
|
||||||
|
got.Status != "passed" || got.Model != "offline-model" || got.Tokens == nil || *got.Tokens != 18 {
|
||||||
|
t.Fatalf("unexpected execution summary: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
id: example.run
|
prompts:
|
||||||
version: "1.0.0"
|
- id: example.run
|
||||||
default_profile: offline-example
|
default_profile: offline-example
|
||||||
description: Run a prompt with a deterministic injected model client.
|
description: Run a prompt with a deterministic injected model client.
|
||||||
inputs:
|
inputs:
|
||||||
- name: note
|
- name: note
|
||||||
required: true
|
required: true
|
||||||
content_type: text/plain
|
content_type: text/plain
|
||||||
messages:
|
messages:
|
||||||
- role: system
|
- role: system
|
||||||
content: Summarize the note in one sentence.
|
content: Summarize the note in one sentence.
|
||||||
- role: user
|
- role: user
|
||||||
content: '{{input "note"}}'
|
content: '{{input "note"}}'
|
||||||
output:
|
output:
|
||||||
format: text
|
format: text
|
||||||
validation_mode: basic
|
validation_mode: basic
|
||||||
|
|||||||
@@ -18,9 +18,8 @@ func (r RunRequest) GoString() string {
|
|||||||
|
|
||||||
func (r RunRequest) redactedString() string {
|
func (r RunRequest) redactedString() string {
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"promptkit.RunRequest{PromptID:%q PromptVersion:%q ProfileID:%q APIKeySet:%t Inputs:%d Vars:%d ExecutionSet:%t ValidationSet:%t AppendedMessages:%d}",
|
"promptkit.RunRequest{PromptID:%q ProfileID:%q APIKeySet:%t Inputs:%d Vars:%d ExecutionSet:%t ValidationSet:%t AppendedMessages:%d}",
|
||||||
r.PromptID,
|
r.PromptID,
|
||||||
r.PromptVersion,
|
|
||||||
r.ProfileID,
|
r.ProfileID,
|
||||||
r.APIKey != "",
|
r.APIKey != "",
|
||||||
len(r.Inputs),
|
len(r.Inputs),
|
||||||
|
|||||||
176
frozen_rendering_contract_test.go
Normal file
176
frozen_rendering_contract_test.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
const capturedPromptDocument = `
|
||||||
|
prompts:
|
||||||
|
- id: captured
|
||||||
|
default_profile: offline
|
||||||
|
session_id: '{{.session}}'
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content_file: body.tmpl
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
`
|
||||||
|
|
||||||
|
func capturedPromptEngine(t *testing.T, source fs.FS) *promptkit.Engine {
|
||||||
|
t.Helper()
|
||||||
|
inputs, err := promptkit.FilesystemInputs("application prompts", source, "config")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("select prompt inputs: %v", err)
|
||||||
|
}
|
||||||
|
endpoint, model := "https://example.invalid/v1", "offline-model"
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{
|
||||||
|
{Inputs: inputs},
|
||||||
|
{Inputs: []configkit.Input{{Source: promptkit.ProfileSource("offline profile", []promptkit.ProfileConfig{
|
||||||
|
{ID: "offline", Endpoint: &endpoint, Model: &model},
|
||||||
|
})}}},
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
return engine
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEngineCapturesPromptContentUntilReconstruction(t *testing.T) {
|
||||||
|
files := fstest.MapFS{
|
||||||
|
"config/prompt.yaml": &fstest.MapFile{Data: []byte(capturedPromptDocument)},
|
||||||
|
"config/body.tmpl": &fstest.MapFile{Data: []byte("before {{.name}}")},
|
||||||
|
}
|
||||||
|
first := capturedPromptEngine(t, files)
|
||||||
|
files["config/body.tmpl"] = &fstest.MapFile{Data: []byte("after {{.name}}")}
|
||||||
|
request := promptkit.RunRequest{PromptID: "captured", Vars: map[string]string{"name": "Ada", "session": "session-1"}}
|
||||||
|
oldPrepared, err := first.Prepare(context.Background(), request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("prepare with existing engine: %v", err)
|
||||||
|
}
|
||||||
|
second := capturedPromptEngine(t, files)
|
||||||
|
newPrepared, err := second.Prepare(context.Background(), request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("prepare with rebuilt engine: %v", err)
|
||||||
|
}
|
||||||
|
if oldPrepared.Messages[0].Content != "before Ada" || newPrepared.Messages[0].Content != "after Ada" || oldPrepared.PromptHash == newPrepared.PromptHash {
|
||||||
|
t.Fatalf("captured content or hash did not follow engine lifetime: old=%+v new=%+v", oldPrepared, newPrepared)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type unreadableAfterConstructionFS struct {
|
||||||
|
fs.FS
|
||||||
|
closed atomic.Bool
|
||||||
|
reads atomic.Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (source *unreadableAfterConstructionFS) Open(name string) (fs.File, error) {
|
||||||
|
if source.closed.Load() {
|
||||||
|
source.reads.Add(1)
|
||||||
|
return nil, fs.ErrPermission
|
||||||
|
}
|
||||||
|
return source.FS.Open(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapturedTemplatesBindInputsIndependentlyAcrossConcurrentPreparations(t *testing.T) {
|
||||||
|
source := &unreadableAfterConstructionFS{FS: fstest.MapFS{
|
||||||
|
"config/prompt.yaml": &fstest.MapFile{Data: []byte(capturedPromptDocument)},
|
||||||
|
"config/body.tmpl": &fstest.MapFile{Data: []byte(`{{.name}}: {{input "note"}}`)},
|
||||||
|
}}
|
||||||
|
engine := capturedPromptEngine(t, source)
|
||||||
|
source.closed.Store(true)
|
||||||
|
|
||||||
|
const calls = 24
|
||||||
|
var wait sync.WaitGroup
|
||||||
|
errs := make(chan error, calls)
|
||||||
|
for index := 0; index < calls; index++ {
|
||||||
|
wait.Add(1)
|
||||||
|
go func(index int) {
|
||||||
|
defer wait.Done()
|
||||||
|
name := fmt.Sprintf("caller-%d", index)
|
||||||
|
body := fmt.Sprintf("body-%d", index)
|
||||||
|
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
|
||||||
|
PromptID: "captured", SessionID: "direct-session",
|
||||||
|
Vars: map[string]string{"name": name},
|
||||||
|
Inputs: map[string]promptkit.ArtifactRef{"note": promptkit.Inline(body)},
|
||||||
|
})
|
||||||
|
if err == nil && (prepared.Messages[0].Content != name+": "+body || prepared.SessionID != "direct-session") {
|
||||||
|
err = fmt.Errorf("unexpected prepared content: %+v", prepared)
|
||||||
|
}
|
||||||
|
errs <- err
|
||||||
|
}(index)
|
||||||
|
}
|
||||||
|
wait.Wait()
|
||||||
|
close(errs)
|
||||||
|
for err := range errs {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if source.reads.Load() != 0 {
|
||||||
|
t.Fatalf("preparation reopened captured source %d times", source.reads.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := engine.Prepare(context.Background(), promptkit.RunRequest{
|
||||||
|
PromptID: "captured", Vars: map[string]string{"name": "Ada"},
|
||||||
|
Inputs: map[string]promptkit.ArtifactRef{"note": promptkit.Inline("body")},
|
||||||
|
})
|
||||||
|
if !errors.Is(err, promptkit.ErrPromptRender) {
|
||||||
|
t.Fatalf("missing session variable = %v, want runtime render error", err)
|
||||||
|
}
|
||||||
|
_, err = engine.Prepare(context.Background(), promptkit.RunRequest{
|
||||||
|
PromptID: "captured", SessionID: "direct-session", Vars: map[string]string{"name": "Ada"},
|
||||||
|
})
|
||||||
|
if !errors.Is(err, promptkit.ErrPromptRender) {
|
||||||
|
t.Fatalf("missing input helper artifact = %v, want runtime render error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreparationStillReadsRequestArtifactsAndSchemasAfterConstruction(t *testing.T) {
|
||||||
|
endpoint, model := "https://example.invalid/v1", "offline-model"
|
||||||
|
schemas := fstest.MapFS{}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: promptkit.ProfileSource("profile", []promptkit.ProfileConfig{
|
||||||
|
{ID: "offline", Endpoint: &endpoint, Model: &model},
|
||||||
|
})}}}},
|
||||||
|
Main: &configkit.Input{Source: configkit.Mapping("structured prompt", map[string]any{
|
||||||
|
"prompts": []any{map[string]any{"id": "structured",
|
||||||
|
"default_profile": "offline",
|
||||||
|
"inputs": []any{map[string]any{"name": "note", "required": true}},
|
||||||
|
"messages": []any{map[string]any{"role": "user", "content": `{{input "note"}}`}},
|
||||||
|
"output": map[string]any{"format": "json", "validation_mode": "json_schema", "schema_path": "schema.json"},
|
||||||
|
}},
|
||||||
|
})},
|
||||||
|
}}, promptkit.WithSchemaFS(schemas, "."))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine before request resources exist: %v", err)
|
||||||
|
}
|
||||||
|
schemas["schema.json"] = &fstest.MapFile{Data: []byte(`{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object"}`)}
|
||||||
|
artifactPath := filepath.Join(t.TempDir(), "note.txt")
|
||||||
|
if err := os.WriteFile(artifactPath, []byte("request artifact"), 0o600); err != nil {
|
||||||
|
t.Fatalf("write request artifact: %v", err)
|
||||||
|
}
|
||||||
|
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "structured", Inputs: map[string]promptkit.ArtifactRef{
|
||||||
|
"note": promptkit.File(artifactPath),
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("prepare with resources supplied after construction: %v", err)
|
||||||
|
}
|
||||||
|
if len(prepared.Messages) != 1 || prepared.Messages[0].Content != "request artifact" || prepared.StructuredOutput == nil || prepared.StructuredOutput.JSONSchema == nil {
|
||||||
|
t.Fatalf("runtime artifact or schema was not prepared: %+v", prepared)
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user