Add generic semantic reconciliation prompt assets

This commit is contained in:
2026-08-09 16:08:53 +00:00
parent 297d58f090
commit b630384aa0
15 changed files with 441 additions and 23 deletions

View File

@@ -11,12 +11,14 @@ import (
)
// PromptAssetManifest is the ordered set of assets that make up one prompt.
// Module files are addressed in the owning module filesystem; shared files use
// the names in sharedPromptPaths and are mounted beneath sharedassets.
// Module files are addressed in the owning module filesystem. SharedFiles use
// names in sharedPromptPaths, while ExternalSharedFiles are explicit
// caller-owned descriptors. Both kinds are mounted beneath sharedassets.
type PromptAssetManifest struct {
ModuleDir string
ModuleFiles []promptfs.ModulePromptFile
SharedFiles []string
ModuleDir string
ModuleFiles []promptfs.ModulePromptFile
SharedFiles []string
ExternalSharedFiles []promptfs.SharedPromptFile
}
var sharedPromptPaths = map[string]string{
@@ -40,7 +42,7 @@ func sharedAssetFS() (fs.FS, error) {
}
func (manifest PromptAssetManifest) PromptFS(moduleFS fs.FS) (fs.FS, error) {
sharedFiles, err := resolveSharedPromptFiles(manifest.SharedFiles)
sharedFiles, err := manifest.sharedPromptFiles()
if err != nil {
return nil, err
}
@@ -49,10 +51,14 @@ func (manifest PromptAssetManifest) PromptFS(moduleFS fs.FS) (fs.FS, error) {
}
func (manifest PromptAssetManifest) Hash(moduleFS fs.FS) (string, error) {
sharedFiles, err := resolveSharedPromptFiles(manifest.SharedFiles)
sharedFiles, err := manifest.sharedPromptFiles()
if err != nil {
return "", err
}
moduleFiles := append([]promptfs.ModulePromptFile(nil), manifest.ModuleFiles...)
if _, err := promptfs.ModulePromptFS(manifest.ModuleDir, moduleFS, moduleFiles, sharedFiles...); err != nil {
return "", err
}
parts := make([]llm.AssetHashPart, 0, len(manifest.ModuleFiles)+len(sharedFiles))
for _, file := range manifest.ModuleFiles {
parts = append(parts, llm.AssetHashPart{FS: moduleFS, Path: file.Path})
@@ -63,6 +69,14 @@ func (manifest PromptAssetManifest) Hash(moduleFS fs.FS) (string, error) {
return llm.HashAssets(parts)
}
func (manifest PromptAssetManifest) sharedPromptFiles() ([]promptfs.SharedPromptFile, error) {
files, err := resolveSharedPromptFiles(manifest.SharedFiles)
if err != nil {
return nil, err
}
return append(files, manifest.ExternalSharedFiles...), nil
}
func resolveSharedPromptFiles(names []string) ([]promptfs.SharedPromptFile, error) {
assets, err := sharedAssetFS()
if err != nil {

View File

@@ -207,3 +207,106 @@ func TestSharedPromptDescriptorsReturnFreshCopies(t *testing.T) {
t.Fatalf("resolveSharedPromptFiles() reused descriptor state: first=%#v second=%#v", first, second)
}
}
func TestPromptAssetManifestMountsExternalSharedFiles(t *testing.T) {
external := fstest.MapFS{"core/protocol.md": {Data: []byte("integer protocol")}}
manifest := PromptAssetManifest{
ModuleDir: "dnd.test",
ModuleFiles: []promptfs.ModulePromptFile{
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
},
ExternalSharedFiles: []promptfs.SharedPromptFile{
{Name: "protocol.md", FS: external, Path: "core/protocol.md"},
},
}
fys, err := manifest.PromptFS(fstest.MapFS{
"prompts/prompt.yaml": {Data: []byte("id: dnd.test")},
})
if err != nil {
t.Fatalf("PromptFS() error = %v, want nil", err)
}
content, err := fs.ReadFile(fys, "assets/prompts/dnd.test/sharedassets/protocol.md")
if err != nil || string(content) != "integer protocol" {
t.Fatalf("mounted external protocol = %q, %v", content, err)
}
}
func TestPromptAssetManifestHashIncludesExternalSharedFiles(t *testing.T) {
moduleFS := fstest.MapFS{"prompts/prompt.yaml": {Data: []byte("id: dnd.test")}}
external := fstest.MapFS{"core/protocol.md": {Data: []byte("integer protocol")}}
manifest := PromptAssetManifest{
ModuleDir: "dnd.test",
ModuleFiles: []promptfs.ModulePromptFile{
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
},
ExternalSharedFiles: []promptfs.SharedPromptFile{
{Name: "protocol.md", FS: external, Path: "core/protocol.md"},
},
}
got, err := manifest.Hash(moduleFS)
if err != nil {
t.Fatalf("Hash() error = %v, want nil", err)
}
want, err := llm.HashAssets([]llm.AssetHashPart{
{FS: moduleFS, Path: "prompts/prompt.yaml"},
{FS: external, Path: "core/protocol.md"},
})
if err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("Hash() = %q, want external-aware hash %q", got, want)
}
}
func TestPromptAssetManifestRejectsInvalidExternalSharedFiles(t *testing.T) {
moduleFS := fstest.MapFS{"prompts/prompt.yaml": {Data: []byte("id: dnd.test")}}
tests := []struct {
name string
external []promptfs.SharedPromptFile
shared []string
want string
}{
{
name: "missing",
external: []promptfs.SharedPromptFile{
{Name: "protocol.md", FS: fstest.MapFS{}, Path: "core/protocol.md"},
},
want: "read shared prompt asset core/protocol.md",
},
{
name: "duplicate external destinations",
external: []promptfs.SharedPromptFile{
{Name: "protocol.md", FS: fstest.MapFS{"a.md": {Data: []byte("a")}}, Path: "a.md"},
{Name: " protocol.md ", FS: fstest.MapFS{"b.md": {Data: []byte("b")}}, Path: "b.md"},
},
want: `duplicate sharedassets prompt destination "assets/prompts/dnd.test/sharedassets/protocol.md"`,
},
{
name: "duplicate named and external destinations",
shared: []string{"common-dnd-system.md"},
external: []promptfs.SharedPromptFile{
{Name: "common-dnd-system.md", FS: fstest.MapFS{"system.md": {Data: []byte("external")}}, Path: "system.md"},
},
want: `duplicate sharedassets prompt destination "assets/prompts/dnd.test/sharedassets/common-dnd-system.md"`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
manifest := PromptAssetManifest{
ModuleDir: "dnd.test",
ModuleFiles: []promptfs.ModulePromptFile{
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
},
SharedFiles: test.shared,
ExternalSharedFiles: test.external,
}
if _, err := manifest.PromptFS(moduleFS); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("PromptFS() error = %v, want %q", err, test.want)
}
if _, err := manifest.Hash(moduleFS); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("Hash() error = %v, want %q", err, test.want)
}
})
}
}

View File

@@ -6,6 +6,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/framework/semanticreconcile"
"gitea.maximumdirect.net/eric/notarius/internal/modules/generic/chunk/units"
jsonoutput "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/output/json"
alwaysaccept "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/validate/always_accept"
@@ -16,14 +17,17 @@ import (
// Register adds all production domain-neutral modules and validators.
func Register(registries pipeline.Registries, assets *llm.AssetRegistry) error {
_ = assets
if err := validateRegistries(registries); err != nil {
return err
}
if assets == nil {
return fmt.Errorf("generic registrar: asset registry must not be nil")
}
registrations := []struct {
name string
register func() error
}{
{name: "semantic reconciliation assets", register: func() error { return semanticreconcile.RegisterAssets(assets) }},
{name: "generic chunker", register: func() error { return units.Register(registries.Chunkers) }},
{name: "always accept validator", register: func() error { return alwaysaccept.Register(registries.Validators) }},
{name: "always reject validator", register: func() error { return alwaysreject.Register(registries.Validators) }},

View File

@@ -1,15 +1,18 @@
package register
import (
"io/fs"
"strings"
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)
func TestRegisterAddsGenericFamily(t *testing.T) {
registries := completeRegistries()
if err := Register(registries, nil); err != nil {
assets := llm.NewAssetRegistry()
if err := Register(registries, assets); err != nil {
t.Fatalf("Register() error = %v, want nil", err)
}
assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"generic"})
@@ -30,6 +33,31 @@ func TestRegisterAddsGenericFamily(t *testing.T) {
if output, err := registries.Outputs.Build("json"); err != nil || output.Key() != "json" {
t.Fatalf("build json output = %v, %v; want json implementation", output, err)
}
promptAssets, err := assets.PromptFS()
if err != nil {
t.Fatal(err)
}
if _, err := fs.ReadFile(promptAssets, "generic.semantic_reconciliation/prompt.yaml"); err != nil {
t.Fatalf("registered semantic reconciliation prompt: %v", err)
}
schemaAssets, err := assets.SchemaFS()
if err != nil {
t.Fatal(err)
}
if _, err := fs.ReadFile(schemaAssets, "semantic_reconciliation_llm.v1.json"); err != nil {
t.Fatalf("registered semantic reconciliation schema: %v", err)
}
}
func TestRegisterRejectsNilAssetRegistryBeforeMutation(t *testing.T) {
registries := completeRegistries()
err := Register(registries, nil)
if err == nil || !strings.Contains(err.Error(), "asset registry must not be nil") {
t.Fatalf("Register() error = %v, want nil asset registry error", err)
}
if len(registries.Chunkers.RegisteredKeys()) != 0 {
t.Fatalf("chunker keys = %#v, want validation before mutation", registries.Chunkers.RegisteredKeys())
}
}
func TestRegisterRejectsMissingGenericRegistriesBeforeMutation(t *testing.T) {
@@ -48,7 +76,7 @@ func TestRegisterRejectsMissingGenericRegistriesBeforeMutation(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
registries := completeRegistries()
test.remove(&registries)
err := Register(registries, nil)
err := Register(registries, llm.NewAssetRegistry())
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("Register() error = %v, want %q", err, test.wantErr)
}
@@ -61,11 +89,12 @@ func TestRegisterRejectsMissingGenericRegistriesBeforeMutation(t *testing.T) {
func TestRegisterReportsDuplicateGenericRegistration(t *testing.T) {
registries := completeRegistries()
if err := Register(registries, nil); err != nil {
assets := llm.NewAssetRegistry()
if err := Register(registries, assets); err != nil {
t.Fatalf("first Register() error = %v, want nil", err)
}
err := Register(registries, nil)
if err == nil || !strings.Contains(err.Error(), "register generic chunker") || !strings.Contains(err.Error(), "already registered") {
err := Register(registries, assets)
if err == nil || !strings.Contains(err.Error(), "register semantic reconciliation assets") || !strings.Contains(err.Error(), "already registered") {
t.Fatalf("second Register() error = %v, want contextual duplicate error", err)
}
}