Finish the domain pipeline cleanup

This commit is contained in:
2026-07-17 11:29:52 -05:00
parent 68481804a7
commit 60b86dc40c
16 changed files with 214 additions and 1130 deletions

View File

@@ -3787,7 +3787,10 @@ func (fakeRunCodec) Kind() contracts.ArtifactKind { return fakeRunArtifactKind }
func (fakeRunCodec) Schema() contracts.ArtifactSchema {
return contracts.ArtifactSchema{ID: "fake.artifact", Name: "fake_artifact", Version: "v1", JSONSchema: []byte(`{"type":"object"}`)}
}
func (fakeRunCodec) MediaType() string { return "application/json" }
func (fakeRunCodec) MediaType() string { return "application/json" }
func (fakeRunCodec) EncodeCandidate(v fakeRunArtifact) ([]byte, error) {
return json.Marshal(v)
}
func (fakeRunCodec) Encode(v fakeRunArtifact) ([]byte, error) { return json.Marshal(v) }
func (fakeRunCodec) Decode(b []byte) (fakeRunArtifact, error) {
var v fakeRunArtifact

View File

@@ -643,6 +643,9 @@ func (fakeArtifactCodec) Schema() contracts.ArtifactSchema {
return contracts.ArtifactSchema{ID: "urn:notarius:test:artifact", Name: "Test artifact", Version: "1", JSONSchema: []byte(`{"type":"string"}`)}
}
func (fakeArtifactCodec) MediaType() string { return "application/json" }
func (fakeArtifactCodec) EncodeCandidate(value fakeArtifact) ([]byte, error) {
return []byte(fmt.Sprintf("%q", value)), nil
}
func (fakeArtifactCodec) Encode(value fakeArtifact) ([]byte, error) {
return []byte(fmt.Sprintf("%q", value)), nil
}

View File

@@ -41,6 +41,10 @@ type ArtifactCodec[T any] interface {
Kind() ArtifactKind
Schema() ArtifactSchema
MediaType() string
// EncodeCandidate serializes a stage result before semantic validation. It
// must not apply validity checks owned by typed validators; Encode remains
// the strict final-artifact boundary used after validation succeeds.
EncodeCandidate(T) ([]byte, error)
Encode(T) ([]byte, error)
Decode([]byte) (T, error)
}

View File

@@ -120,19 +120,16 @@ func RegisterArtifactCodec[T any](registry *ArtifactCodecRegistry, codec contrac
return decoded, nil
},
}
entry.encodeCandidate = entry.encode
if candidate, ok := any(codec).(interface{ EncodeCandidate(T) ([]byte, error) }); ok {
entry.encodeCandidate = func(value any) ([]byte, error) {
typed, err := exactTypedValue[T]("encode candidate artifact", value)
if err != nil {
return nil, err
}
content, err := candidate.EncodeCandidate(typed)
if err != nil {
return nil, &ArtifactCodecOperationError{Operation: "encode", Kind: spec.Kind, Err: err}
}
return append([]byte(nil), content...), nil
entry.encodeCandidate = func(value any) ([]byte, error) {
typed, err := exactTypedValue[T]("encode candidate artifact", value)
if err != nil {
return nil, err
}
content, err := codec.EncodeCandidate(typed)
if err != nil {
return nil, &ArtifactCodecOperationError{Operation: "encode candidate", Kind: spec.Kind, Err: err}
}
return append([]byte(nil), content...), nil
}
if provider, ok := any(codec).(interface{ Metadata(T) map[string]any }); ok {
entry.metadata = func(value any) map[string]any {

View File

@@ -24,19 +24,28 @@ type codecScore struct {
type codecNotesAlias codecNotes
type testArtifactCodec[T any] struct {
kind contracts.ArtifactKind
schema contracts.ArtifactSchema
mediaType string
encodeFunc func(T) ([]byte, error)
decodeFunc func([]byte) (T, error)
kind contracts.ArtifactKind
schema contracts.ArtifactSchema
mediaType string
encodeFunc func(T) ([]byte, error)
candidateFunc func(T) ([]byte, error)
decodeFunc func([]byte) (T, error)
}
func (c testArtifactCodec[T]) Kind() contracts.ArtifactKind { return c.kind }
func (c testArtifactCodec[T]) Schema() contracts.ArtifactSchema { return c.schema }
func (c testArtifactCodec[T]) MediaType() string { return c.mediaType }
func (c testArtifactCodec[T]) EncodeCandidate(value T) ([]byte, error) {
if c.candidateFunc != nil {
return c.candidateFunc(value)
}
return c.encodeFunc(value)
}
func (c testArtifactCodec[T]) Encode(value T) ([]byte, error) { return c.encodeFunc(value) }
func (c testArtifactCodec[T]) Decode(content []byte) (T, error) { return c.decodeFunc(content) }
var _ contracts.ArtifactCodec[codecNotes] = testArtifactCodec[codecNotes]{}
func TestArtifactCodecRegistryStoresHeterogeneousExactTypes(t *testing.T) {
registry := NewArtifactCodecRegistry()
if err := RegisterArtifactCodec(registry, notesCodec()); err != nil {
@@ -92,6 +101,43 @@ func TestArtifactCodecRegistryStoresHeterogeneousExactTypes(t *testing.T) {
}
}
func TestArtifactCodecRegistryKeepsCandidateAndFinalEncodingDistinct(t *testing.T) {
candidateCalls, finalCalls := 0, 0
codec := notesCodec()
codec.candidateFunc = func(codecNotes) ([]byte, error) {
candidateCalls++
return []byte(`{"items":["candidate"]}`), nil
}
codec.encodeFunc = func(codecNotes) ([]byte, error) {
finalCalls++
return []byte(`{"items":["final"]}`), nil
}
registry := NewArtifactCodecRegistry()
if err := RegisterArtifactCodec(registry, codec); err != nil {
t.Fatalf("RegisterArtifactCodec() error = %v, want nil", err)
}
entry, _, err := registry.entry(codec.kind)
if err != nil {
t.Fatalf("entry() error = %v, want nil", err)
}
candidate, err := serializeArtifact(entry, codecNotes{}, true)
if err != nil {
t.Fatalf("serialize candidate error = %v, want nil", err)
}
if string(candidate.Content) != `{"items":["candidate"]}` || candidateCalls != 1 || finalCalls != 0 {
t.Fatalf("candidate content = %s, calls = candidate %d, final %d", candidate.Content, candidateCalls, finalCalls)
}
final, err := serializeArtifact(entry, codecNotes{}, false)
if err != nil {
t.Fatalf("serialize final error = %v, want nil", err)
}
if string(final.Content) != `{"items":["final"]}` || candidateCalls != 1 || finalCalls != 1 {
t.Fatalf("final content = %s, calls = candidate %d, final %d", final.Content, candidateCalls, finalCalls)
}
}
func TestArtifactCodecRegistryStoresValidatedSchemaMetadata(t *testing.T) {
registry := NewArtifactCodecRegistry()
codec := notesCodec()

View File

@@ -409,6 +409,17 @@ type attemptTerminalRecorder struct {
envelope debugTimedEnvelope
}
type attemptDebugPersistenceError struct {
label string
err error
}
func (e *attemptDebugPersistenceError) Error() string {
return fmt.Sprintf("write %s attempt debug artifact: %v", e.label, e.err)
}
func (e *attemptDebugPersistenceError) Unwrap() error { return e.err }
func newAttemptTerminalRecorder(recorder DebugRecorder, attemptPath, label string, scope *debugLLMScope, envelope debugTimedEnvelope) attemptTerminalRecorder {
return attemptTerminalRecorder{recorder: recorder, path: attemptPath, label: label, scope: scope, envelope: envelope}
}
@@ -420,7 +431,7 @@ func (r attemptTerminalRecorder) record(payload any, terminalErr error) error {
envelope.Error = terminalErr.Error()
}
if err := writeDebugAttempt(r.recorder, r.path, envelope, r.scope); err != nil {
debugErr := fmt.Errorf("write %s attempt debug artifact: %w", r.label, err)
debugErr := &attemptDebugPersistenceError{label: r.label, err: err}
return errors.Join(terminalErr, debugErr)
}
return terminalErr

View File

@@ -143,7 +143,10 @@ func (defaultArtifactCodec) Kind() contracts.ArtifactKind { return defaultArtifa
func (defaultArtifactCodec) Schema() contracts.ArtifactSchema {
return contracts.ArtifactSchema{ID: "urn:notarius:test:default", Name: "default", Version: "1", JSONSchema: []byte(`{"type":"object"}`)}
}
func (defaultArtifactCodec) MediaType() string { return "application/json" }
func (defaultArtifactCodec) MediaType() string { return "application/json" }
func (defaultArtifactCodec) EncodeCandidate(value defaultArtifact) ([]byte, error) {
return json.Marshal(value)
}
func (defaultArtifactCodec) Encode(value defaultArtifact) ([]byte, error) { return json.Marshal(value) }
func (defaultArtifactCodec) Decode(content []byte) (defaultArtifact, error) {
var value defaultArtifact

View File

@@ -357,6 +357,10 @@ func runWithRetry(ctx context.Context, retries int, run func(attempt int) (bool,
}
accepted, rejection, err := run(attempt)
if err != nil {
var debugErr *attemptDebugPersistenceError
if errors.As(err, &debugErr) {
return false, nil, fmt.Errorf("failed after %d attempt(s): %w", attempt, err)
}
if attempt == attempts {
return false, nil, fmt.Errorf("failed after %d attempt(s): %w", attempt, err)
}

View File

@@ -18,6 +18,7 @@ type terminalChunker struct {
chunks []source.Chunk
warnings []contracts.Warning
err error
calls *int
}
func (c terminalChunker) Key() string { return c.key }
@@ -25,6 +26,9 @@ func (c terminalChunker) Key() string { return c.key }
func (terminalChunker) ReferenceSlots() []contracts.ReferenceSlot { return nil }
func (c terminalChunker) Chunk(context.Context, contracts.ChunkRequest) (contracts.ChunkResult, error) {
if c.calls != nil {
(*c.calls)++
}
return contracts.ChunkResult{Chunks: cloneSourceChunks(c.chunks), Warnings: cloneWarnings(c.warnings)}, c.err
}
@@ -211,6 +215,30 @@ func TestRunnerJoinsPrimaryAndAttemptWriteErrors(t *testing.T) {
})
}
func TestRunnerDoesNotRetryAfterTerminalAttemptWriteFailure(t *testing.T) {
prepared, chunks := preparedTerminalDebugPipeline(t)
prepared.resolved.Chunk.Retries = 1
calls := 0
prepared.chunker = terminalChunker{key: prepared.resolved.Chunk.Module, chunks: chunks, calls: &calls}
prepared.chunkValidators.validators = []preparedValidator{{
resolved: ResolvedValidator{Binding: Binding("terminal/chunk-validator"), Target: ValidatorTargetChunk},
chunk: terminalChunkValidator{result: contracts.ValidationResult{Approved: true}},
}}
debug := newCapturedDebugRecorder()
debug.failPath = "chunk/attempt-01.json"
_, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), Debug: debug})
if err == nil || !strings.Contains(err.Error(), "write chunk attempt debug artifact") || !strings.Contains(err.Error(), "debug recorder failure") {
t.Fatalf("Run() error = %v, want terminal attempt debug failure", err)
}
if calls != 1 {
t.Fatalf("chunk calls = %d, want one attempt without retry", calls)
}
if debug.has("chunk/attempt-02.json") {
t.Fatal("second chunk attempt envelope exists after non-retryable debug persistence failure")
}
}
func TestRunnerKeepsExtractModuleAndValidatorLLMCallsIsolated(t *testing.T) {
prepared := preparedAttemptDebugPipeline(t)
debug := newCapturedDebugRecorder()

View File

@@ -189,6 +189,41 @@ func TestImportBoundaryRules(t *testing.T) {
sourcePackage: "cli",
importPath: moduleImportPrefix + "almanac/extract/events",
},
{
name: "command production cannot import registrar",
filename: "cmd/notarius/main.go",
sourcePackage: "main",
importPath: moduleImportPrefix + "almanac/register",
wantError: true,
},
{
name: "command production cannot import concrete leaf",
filename: "cmd/notarius/main.go",
sourcePackage: "main",
importPath: moduleImportPrefix + "almanac/extract/events",
wantError: true,
},
{
name: "unknown production package cannot import concrete leaf",
filename: "internal/application/bootstrap.go",
sourcePackage: "application",
importPath: moduleImportPrefix + "almanac/extract/events",
wantError: true,
},
{
name: "unknown production package cannot import registrar",
filename: "internal/application/bootstrap.go",
sourcePackage: "application",
importPath: moduleImportPrefix + "almanac/register",
wantError: true,
},
{
name: "unknown test package is not a compatibility root",
filename: "internal/application/bootstrap_test.go",
sourcePackage: "application",
importPath: moduleImportPrefix + "almanac/extract/events",
wantError: true,
},
{
name: "framework production cannot import concrete module",
filename: "internal/framework/pipeline/runner.go",
@@ -324,35 +359,41 @@ func validateImport(filename string, sourcePackage string, importPath string) er
}
return importBoundaryViolation(filename, importPath, "module integration composition is allowed only in black-box tests")
}
if !isTest && (strings.HasPrefix(filename, "internal/framework/") || strings.HasPrefix(filename, "internal/core/")) {
sourceFamily, sourceRoot, sourceRegistrar := moduleFamilyForFile(filename)
if sourceFamily != "" {
if sourceRoot && sourceFamily == target.family && target.child {
return importBoundaryViolation(filename, importPath, "family root must not import child packages")
}
if sourceFamily == target.family {
return nil
}
if sourceFamily == "generic" {
return importBoundaryViolation(filename, importPath, fmt.Sprintf("generic family must not import concrete family %q", target.family))
}
if target.family == "generic" {
if sourceRegistrar {
return nil
}
return importBoundaryViolation(filename, importPath, fmt.Sprintf("concrete family %q may import generic implementations only from its registrar", sourceFamily))
}
return importBoundaryViolation(filename, importPath, fmt.Sprintf("concrete family %q must not import concrete family %q", sourceFamily, target.family))
}
if isTest {
if isCompatibilityTestFile(filename) {
return nil
}
return importBoundaryViolation(filename, importPath, "direct module imports from non-module tests are allowed only in CLI, core, and framework compatibility-test roots")
}
if strings.HasPrefix(filename, "internal/framework/") || strings.HasPrefix(filename, "internal/core/") {
return importBoundaryViolation(filename, importPath, "core and framework production code must not import module implementations")
}
if !isTest && strings.HasPrefix(filename, "internal/cli/") {
if strings.HasPrefix(filename, "internal/cli/") {
if target.registrar {
return nil
}
return importBoundaryViolation(filename, importPath, "CLI production code may import only exact module family registrar packages")
}
sourceFamily, sourceRoot, sourceRegistrar := moduleFamilyForFile(filename)
if sourceFamily == "" {
return nil
}
if sourceRoot && sourceFamily == target.family && target.child {
return importBoundaryViolation(filename, importPath, "family root must not import child packages")
}
if sourceFamily == target.family {
return nil
}
if sourceFamily == "generic" {
return importBoundaryViolation(filename, importPath, fmt.Sprintf("generic family must not import concrete family %q", target.family))
}
if target.family == "generic" {
if sourceRegistrar {
return nil
}
return importBoundaryViolation(filename, importPath, fmt.Sprintf("concrete family %q may import generic implementations only from its registrar", sourceFamily))
}
return importBoundaryViolation(filename, importPath, fmt.Sprintf("concrete family %q must not import concrete family %q", sourceFamily, target.family))
return importBoundaryViolation(filename, importPath, "production code outside module families may import modules only from the CLI composition root through exact registrar packages")
}
type moduleImportTarget struct {
@@ -404,6 +445,15 @@ func isBlackBoxIntegrationTest(filename string, sourcePackage string) bool {
return isIntegrationFile(filename) && strings.HasSuffix(filename, "_test.go") && sourcePackage == "integration_test"
}
func isCompatibilityTestFile(filename string) bool {
if !strings.HasSuffix(filename, "_test.go") {
return false
}
return strings.HasPrefix(filename, "internal/cli/") ||
strings.HasPrefix(filename, "internal/core/") ||
strings.HasPrefix(filename, "internal/framework/")
}
func testRepositoryRoot(t *testing.T) string {
t.Helper()
_, filename, _, ok := runtime.Caller(0)

View File

@@ -293,6 +293,9 @@ func (seriatimArtifactCodec) Schema() contracts.ArtifactSchema {
return contracts.ArtifactSchema{ID: "fake.event", Name: "fake_event", Version: "v1", JSONSchema: []byte(`{"type":"object"}`)}
}
func (seriatimArtifactCodec) MediaType() string { return "application/json" }
func (seriatimArtifactCodec) EncodeCandidate(value seriatimArtifact) ([]byte, error) {
return json.Marshal(value)
}
func (seriatimArtifactCodec) Encode(value seriatimArtifact) ([]byte, error) {
return json.Marshal(value)
}