Complete Phase 11 proposal generation framework

This commit is contained in:
2026-05-12 02:25:33 +00:00
parent 12202508bf
commit b360493cdc
12 changed files with 1032 additions and 56 deletions

View File

@@ -0,0 +1,153 @@
package modules
import (
"context"
"fmt"
"strings"
"gitea.maximumdirect.net/eric/audita/internal/core/config"
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
)
const (
ModuleKeyGlossary = "glossary"
ModuleKeyHomophones = "homophones"
ModuleKeySpokenWord = "spoken_word"
ModuleKeyGrammar = "grammar"
)
const (
ReasonUnsupportedModule = "unsupported_module"
ReasonUnimplementedModule = "unimplemented_module"
)
var knownModuleKeys = map[string]struct{}{
ModuleKeyGlossary: {},
ModuleKeyHomophones: {},
ModuleKeySpokenWord: {},
ModuleKeyGrammar: {},
}
// IsKnownModuleKey reports whether a module key is recognized by the production
// registry scaffold.
func IsKnownModuleKey(key string) bool {
_, ok := knownModuleKeys[strings.TrimSpace(key)]
return ok
}
// Dependencies holds explicit constructor dependencies for module creation.
type Dependencies struct {
Config *config.Config
Glossary *schema.Glossary
ProposalLLMClient contracts.StructuredLLMClient
ProposalLLMScheduler contracts.LLMScheduler
ValidationLLMClient contracts.StructuredLLMClient
ValidationLLMScheduler contracts.LLMScheduler
DiagnosticsDir string
}
// ConstructRequest is one module-construction request.
type ConstructRequest struct {
RunSpec contracts.ModuleRunSpec
Dependencies
}
// Constructor builds one module instance from a run spec and explicit deps.
type Constructor func(ctx context.Context, req ConstructRequest) (contracts.TranscriptModule, error)
// Factory resolves configured module specs into module instances.
type Factory struct {
deps Dependencies
constructors map[string]Constructor
}
// NewFactory creates a production registry scaffold with known module keys but
// no real module constructors registered yet.
func NewFactory(deps Dependencies) *Factory {
return &Factory{
deps: deps,
constructors: make(map[string]Constructor, len(knownModuleKeys)),
}
}
// RegisterConstructor registers a constructor for a known module key.
func (f *Factory) RegisterConstructor(moduleKey string, constructor Constructor) error {
if f == nil {
return fmt.Errorf("module factory is nil")
}
key := strings.TrimSpace(moduleKey)
if !IsKnownModuleKey(key) {
return &UnsupportedModuleError{ModuleKey: key}
}
if constructor == nil {
return fmt.Errorf("constructor for module %q must not be nil", key)
}
f.constructors[key] = constructor
return nil
}
// ModuleForSpec resolves one configured run spec into a module instance.
func (f *Factory) ModuleForSpec(spec contracts.ModuleRunSpec) (contracts.TranscriptModule, error) {
if f == nil {
return nil, fmt.Errorf("module factory is nil")
}
key := strings.TrimSpace(spec.ModuleKey)
if !IsKnownModuleKey(key) {
return nil, &UnsupportedModuleError{ModuleKey: key}
}
constructor, ok := f.constructors[key]
if !ok || constructor == nil {
return nil, &UnimplementedModuleError{ModuleKey: key}
}
module, err := constructor(context.Background(), ConstructRequest{
RunSpec: spec,
Dependencies: Dependencies{
Config: f.deps.Config,
Glossary: f.deps.Glossary,
ProposalLLMClient: f.deps.ProposalLLMClient,
ProposalLLMScheduler: f.deps.ProposalLLMScheduler,
ValidationLLMClient: f.deps.ValidationLLMClient,
ValidationLLMScheduler: f.deps.ValidationLLMScheduler,
DiagnosticsDir: f.deps.DiagnosticsDir,
},
})
if err != nil {
return nil, fmt.Errorf("construct module %q: %w", spec.InstanceName, err)
}
if module == nil {
return nil, fmt.Errorf("constructor for module %q returned nil module", key)
}
return module, nil
}
// UnsupportedModuleError indicates a configured module key is unknown.
type UnsupportedModuleError struct {
ModuleKey string
}
func (e *UnsupportedModuleError) Error() string {
return fmt.Sprintf("unsupported module key %q", strings.TrimSpace(e.ModuleKey))
}
// ReasonCode returns a stable reason code suitable for reporting.
func (e *UnsupportedModuleError) ReasonCode() string {
return ReasonUnsupportedModule
}
// UnimplementedModuleError indicates a known module key without constructor.
type UnimplementedModuleError struct {
ModuleKey string
}
func (e *UnimplementedModuleError) Error() string {
return fmt.Sprintf("module %q is recognized but not implemented", strings.TrimSpace(e.ModuleKey))
}
// ReasonCode returns a stable reason code suitable for reporting.
func (e *UnimplementedModuleError) ReasonCode() string {
return ReasonUnimplementedModule
}

View File

@@ -0,0 +1,125 @@
package modules
import (
"context"
"errors"
"testing"
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
)
type noopModule struct {
key string
}
func (m noopModule) Key() string { return m.key }
func (m noopModule) ReplacementPolicy() proposals.ReplacementPolicy {
return proposals.ReplacementPolicyRequireUnique
}
func (m noopModule) Validators() []contracts.Validator { return nil }
func (m noopModule) Propose(ctx context.Context, req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
_ = ctx
_ = req
return nil, nil
}
func TestKnownModuleKeyRecognition(t *testing.T) {
for _, key := range []string{ModuleKeyGlossary, ModuleKeyHomophones, ModuleKeySpokenWord, ModuleKeyGrammar} {
if !IsKnownModuleKey(key) {
t.Fatalf("expected key %q to be recognized", key)
}
}
}
func TestUnknownModuleKeyNotRecognized(t *testing.T) {
if IsKnownModuleKey("made_up") {
t.Fatal("expected unknown key to be unrecognized")
}
}
func TestRepeatedRunSpecNamingRemainsDeterministic(t *testing.T) {
specs, err := contracts.ResolveModuleRunSpecs([]string{"glossary", "glossary", "grammar"})
if err != nil {
t.Fatalf("ResolveModuleRunSpecs error: %v", err)
}
if specs[0].InstanceName != "glossary_1" || specs[1].InstanceName != "glossary_2" || specs[2].InstanceName != "grammar" {
t.Fatalf("unexpected instance names: %+v", specs)
}
}
func TestUnsupportedUnknownModuleKeyFailsCleanly(t *testing.T) {
factory := NewFactory(Dependencies{})
_, err := factory.ModuleForSpec(contracts.ModuleRunSpec{ModuleKey: "unknown", InstanceName: "unknown"})
if err == nil {
t.Fatal("expected unsupported-module error")
}
var unsupported *UnsupportedModuleError
if !errors.As(err, &unsupported) {
t.Fatalf("expected UnsupportedModuleError, got %T (%v)", err, err)
}
if unsupported.ReasonCode() != ReasonUnsupportedModule {
t.Fatalf("unexpected reason code: %q", unsupported.ReasonCode())
}
}
func TestRecognizedButUnimplementedModuleKeyFailsCleanly(t *testing.T) {
factory := NewFactory(Dependencies{})
for _, key := range []string{ModuleKeyGlossary, ModuleKeyHomophones, ModuleKeySpokenWord, ModuleKeyGrammar} {
t.Run(key, func(t *testing.T) {
_, err := factory.ModuleForSpec(contracts.ModuleRunSpec{ModuleKey: key, InstanceName: key})
if err == nil {
t.Fatal("expected unimplemented-module error")
}
var unimplemented *UnimplementedModuleError
if !errors.As(err, &unimplemented) {
t.Fatalf("expected UnimplementedModuleError, got %T (%v)", err, err)
}
if unimplemented.ReasonCode() != ReasonUnimplementedModule {
t.Fatalf("unexpected reason code: %q", unimplemented.ReasonCode())
}
})
}
}
func TestRegisterConstructorAndConstruct(t *testing.T) {
factory := NewFactory(Dependencies{})
if err := factory.RegisterConstructor(ModuleKeyGlossary, func(ctx context.Context, req ConstructRequest) (contracts.TranscriptModule, error) {
_ = ctx
if req.RunSpec.InstanceName != "glossary_1" {
t.Fatalf("expected run spec instance name, got %q", req.RunSpec.InstanceName)
}
return noopModule{key: req.RunSpec.ModuleKey}, nil
}); err != nil {
t.Fatalf("RegisterConstructor error: %v", err)
}
module, err := factory.ModuleForSpec(contracts.ModuleRunSpec{
ModuleKey: ModuleKeyGlossary,
InstanceName: "glossary_1",
})
if err != nil {
t.Fatalf("ModuleForSpec error: %v", err)
}
if module.Key() != ModuleKeyGlossary {
t.Fatalf("unexpected module key %q", module.Key())
}
}
func TestRegisterConstructorRejectsUnknownModuleKey(t *testing.T) {
factory := NewFactory(Dependencies{})
err := factory.RegisterConstructor("unknown", func(ctx context.Context, req ConstructRequest) (contracts.TranscriptModule, error) {
_ = ctx
_ = req
return noopModule{key: "unknown"}, nil
})
if err == nil {
t.Fatal("expected register failure for unknown key")
}
var unsupported *UnsupportedModuleError
if !errors.As(err, &unsupported) {
t.Fatalf("expected UnsupportedModuleError, got %T (%v)", err, err)
}
}