Expose chunk plan provenance and safe diagnostics
This commit is contained in:
@@ -709,7 +709,7 @@ refresh have exact state semantics, and the repository is green.
|
|||||||
|
|
||||||
## Stage 6: Expose producer provenance and safe diagnostics
|
## Stage 6: Expose producer provenance and safe diagnostics
|
||||||
|
|
||||||
**Status:** Not started
|
**Status:** Complete
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
|
|
||||||
|
|||||||
@@ -331,6 +331,9 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if output.Manifest.PipelineID != "" && runDir != nil {
|
if output.Manifest.PipelineID != "" && runDir != nil {
|
||||||
_ = runDir.WriteRunManifest(output.Manifest)
|
_ = runDir.WriteRunManifest(output.Manifest)
|
||||||
|
if output.ChunkPlan != nil {
|
||||||
|
_ = runDir.WriteChunkPlan(*output.ChunkPlan)
|
||||||
|
}
|
||||||
_ = runDir.WriteCheckpointEvents(output.CheckpointEvents)
|
_ = runDir.WriteCheckpointEvents(output.CheckpointEvents)
|
||||||
}
|
}
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("run pipeline %q: %w", pipelineID, err))
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("run pipeline %q: %w", pipelineID, err))
|
||||||
@@ -340,6 +343,11 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
if err := writeDiagnostics(runDir, func() error { return runDir.WriteRunManifest(output.Manifest) }); err != nil {
|
if err := writeDiagnostics(runDir, func() error { return runDir.WriteRunManifest(output.Manifest) }); err != nil {
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics run manifest: %w", err))
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics run manifest: %w", err))
|
||||||
}
|
}
|
||||||
|
if output.ChunkPlan != nil {
|
||||||
|
if err := writeDiagnostics(runDir, func() error { return runDir.WriteChunkPlan(*output.ChunkPlan) }); err != nil {
|
||||||
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics chunk plan: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := writeDiagnostics(runDir, func() error { return runDir.WriteWarnings(output.Warnings) }); err != nil {
|
if err := writeDiagnostics(runDir, func() error { return runDir.WriteWarnings(output.Warnings) }); err != nil {
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics warnings: %w", err))
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics warnings: %w", err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,12 +69,42 @@ type RejectedOutputManifest struct {
|
|||||||
DiagnosticArtifactPath string `json:"diagnostic_artifact_path,omitempty"`
|
DiagnosticArtifactPath string `json:"diagnostic_artifact_path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChunkPlanManifest struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Action string `json:"action,omitempty"`
|
||||||
|
SourceDigest string `json:"source_digest,omitempty"`
|
||||||
|
PlanDigest string `json:"plan_digest,omitempty"`
|
||||||
|
PlanSchemaVersion string `json:"plan_schema_version,omitempty"`
|
||||||
|
RequestedModule string `json:"requested_module"`
|
||||||
|
ProducerInputModule string `json:"producer_input_module,omitempty"`
|
||||||
|
ProducerModule string `json:"producer_module,omitempty"`
|
||||||
|
ProducerLLMProfile string `json:"producer_llm_profile,omitempty"`
|
||||||
|
ProducerReferences []ReferenceProvenance `json:"producer_references,omitempty"`
|
||||||
|
ProducerMetadata map[string]any `json:"producer_metadata,omitempty"`
|
||||||
|
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChunkPlanSummary is deliberately limited to cache and validation decisions.
|
||||||
|
// It must never contain plan units, source content, annotations, or model I/O.
|
||||||
|
type ChunkPlanSummary struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
SourceDigest string `json:"source_digest,omitempty"`
|
||||||
|
CandidateDigest string `json:"candidate_digest,omitempty"`
|
||||||
|
RequestedModule string `json:"requested_module"`
|
||||||
|
LookupStatus string `json:"lookup_status"`
|
||||||
|
LookupReason string `json:"lookup_reason,omitempty"`
|
||||||
|
Action string `json:"action,omitempty"`
|
||||||
|
ValidationStatus string `json:"validation_status"`
|
||||||
|
PublicationStatus string `json:"publication_status"`
|
||||||
|
}
|
||||||
|
|
||||||
type RunManifest struct {
|
type RunManifest struct {
|
||||||
RunID string `json:"run_id,omitempty"`
|
RunID string `json:"run_id,omitempty"`
|
||||||
PipelineID string `json:"pipeline_id,omitempty"`
|
PipelineID string `json:"pipeline_id,omitempty"`
|
||||||
PipelineDigest string `json:"pipeline_digest,omitempty"`
|
PipelineDigest string `json:"pipeline_digest,omitempty"`
|
||||||
InputModule string `json:"input_module,omitempty"`
|
InputModule string `json:"input_module,omitempty"`
|
||||||
Chunker string `json:"chunker,omitempty"`
|
Chunker string `json:"chunker,omitempty"`
|
||||||
|
ChunkPlan *ChunkPlanManifest `json:"chunk_plan,omitempty"`
|
||||||
SourceDigests []string `json:"source_digests,omitempty"`
|
SourceDigests []string `json:"source_digests,omitempty"`
|
||||||
Extractors []string `json:"extractors,omitempty"`
|
Extractors []string `json:"extractors,omitempty"`
|
||||||
Merger string `json:"merger,omitempty"`
|
Merger string `json:"merger,omitempty"`
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package artifacts
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,6 +17,37 @@ func TestRunManifestOmitsEmptyOptionalFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunManifestChunkPlanIsAdditiveAndOmitsPlanContent(t *testing.T) {
|
||||||
|
manifest := RunManifest{ChunkPlan: &ChunkPlanManifest{
|
||||||
|
Mode: "auto", Action: "reused", SourceDigest: "sha256:source", PlanDigest: "sha256:plan",
|
||||||
|
PlanSchemaVersion: "notarius.chunk-plan.v1", RequestedModule: "chunk/current",
|
||||||
|
ProducerInputModule: "input/original", ProducerModule: "chunk/original",
|
||||||
|
}}
|
||||||
|
encoded, err := json.Marshal(manifest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
text := string(encoded)
|
||||||
|
for _, want := range []string{`"chunk_plan"`, `"action":"reused"`, `"requested_module":"chunk/current"`, `"producer_module":"chunk/original"`} {
|
||||||
|
if !strings.Contains(text, want) {
|
||||||
|
t.Fatalf("manifest JSON %s does not contain %s", text, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, forbidden := range []string{`"plan"`, `"units"`, `"annotations"`} {
|
||||||
|
if strings.Contains(text, forbidden) {
|
||||||
|
t.Fatalf("manifest JSON contains forbidden field %s: %s", forbidden, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var legacy RunManifest
|
||||||
|
if err := json.Unmarshal([]byte(`{"pipeline_id":"legacy"}`), &legacy); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if legacy.PipelineID != "legacy" || legacy.ChunkPlan != nil {
|
||||||
|
t.Fatalf("legacy manifest = %#v", legacy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunManifestIncludesPipelineAndArtifactLaneFields(t *testing.T) {
|
func TestRunManifestIncludesPipelineAndArtifactLaneFields(t *testing.T) {
|
||||||
manifest := RunManifest{
|
manifest := RunManifest{
|
||||||
PipelineID: "pipeline-1",
|
PipelineID: "pipeline-1",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const (
|
|||||||
ArtifactCheckpointEvents = "checkpoint-events.json"
|
ArtifactCheckpointEvents = "checkpoint-events.json"
|
||||||
ArtifactSourceDocument = "source-document.json"
|
ArtifactSourceDocument = "source-document.json"
|
||||||
ArtifactRunManifest = "run-manifest.json"
|
ArtifactRunManifest = "run-manifest.json"
|
||||||
|
ArtifactChunkPlan = "chunk-plan.json"
|
||||||
ArtifactRunReport = "run-report.json"
|
ArtifactRunReport = "run-report.json"
|
||||||
ArtifactWarnings = "warnings.json"
|
ArtifactWarnings = "warnings.json"
|
||||||
ArtifactErrorLog = "error.log"
|
ArtifactErrorLog = "error.log"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ func TestArtifactNamesUseExtractionOrientedNames(t *testing.T) {
|
|||||||
ArtifactResolvedReferences,
|
ArtifactResolvedReferences,
|
||||||
ArtifactSourceDocument,
|
ArtifactSourceDocument,
|
||||||
ArtifactRunManifest,
|
ArtifactRunManifest,
|
||||||
|
ArtifactChunkPlan,
|
||||||
ArtifactRunReport,
|
ArtifactRunReport,
|
||||||
ArtifactWarnings,
|
ArtifactWarnings,
|
||||||
ArtifactErrorLog,
|
ArtifactErrorLog,
|
||||||
|
|||||||
@@ -167,6 +167,10 @@ func (r *RunDirectory) WriteRunManifest(manifest artifacts.RunManifest) error {
|
|||||||
return r.WriteJSONArtifact(ArtifactRunManifest, manifest)
|
return r.WriteJSONArtifact(ArtifactRunManifest, manifest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RunDirectory) WriteChunkPlan(summary artifacts.ChunkPlanSummary) error {
|
||||||
|
return r.WriteJSONArtifact(ArtifactChunkPlan, summary)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RunDirectory) WriteRunReport(payload any) error {
|
func (r *RunDirectory) WriteRunReport(payload any) error {
|
||||||
return r.WriteJSONArtifact(ArtifactRunReport, payload)
|
return r.WriteJSONArtifact(ArtifactRunReport, payload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,9 @@ func TestWriteTypedArtifacts(t *testing.T) {
|
|||||||
if err := runDir.WriteRunManifest(artifacts.RunManifest{RunID: "run-1"}); err != nil {
|
if err := runDir.WriteRunManifest(artifacts.RunManifest{RunID: "run-1"}); err != nil {
|
||||||
t.Fatalf("WriteRunManifest: %v", err)
|
t.Fatalf("WriteRunManifest: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := runDir.WriteChunkPlan(artifacts.ChunkPlanSummary{Mode: "auto", RequestedModule: "chunk/test", LookupStatus: "invalid", LookupReason: "stored chunk plan failed validation", ValidationStatus: "not_run", PublicationStatus: "not_published"}); err != nil {
|
||||||
|
t.Fatalf("WriteChunkPlan: %v", err)
|
||||||
|
}
|
||||||
if err := runDir.WriteRunReport(map[string]any{"ok": true}); err != nil {
|
if err := runDir.WriteRunReport(map[string]any{"ok": true}); err != nil {
|
||||||
t.Fatalf("WriteRunReport: %v", err)
|
t.Fatalf("WriteRunReport: %v", err)
|
||||||
}
|
}
|
||||||
@@ -213,6 +216,7 @@ func TestWriteTypedArtifacts(t *testing.T) {
|
|||||||
ArtifactResolvedReferences,
|
ArtifactResolvedReferences,
|
||||||
ArtifactSourceDocument,
|
ArtifactSourceDocument,
|
||||||
ArtifactRunManifest,
|
ArtifactRunManifest,
|
||||||
|
ArtifactChunkPlan,
|
||||||
ArtifactRunReport,
|
ArtifactRunReport,
|
||||||
ArtifactWarnings,
|
ArtifactWarnings,
|
||||||
} {
|
} {
|
||||||
@@ -220,6 +224,15 @@ func TestWriteTypedArtifacts(t *testing.T) {
|
|||||||
t.Fatalf("expected artifact %q: %v", name, err)
|
t.Fatalf("expected artifact %q: %v", name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
chunkSummary, err := os.ReadFile(filepath.Join(runDir.Path(), ArtifactChunkPlan))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, forbidden := range []string{"units", "annotations", "source content", "prompt", "response", "raw invalid"} {
|
||||||
|
if strings.Contains(string(chunkSummary), forbidden) {
|
||||||
|
t.Fatalf("chunk plan summary leaked %q: %s", forbidden, chunkSummary)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWriteRedactedEffectiveConfigWritesPayloadReturnedByProvider(t *testing.T) {
|
func TestWriteRedactedEffectiveConfigWritesPayloadReturnedByProvider(t *testing.T) {
|
||||||
|
|||||||
@@ -214,6 +214,10 @@ const (
|
|||||||
ExecutionClassLLMBacked ExecutionClass = "llm_backed"
|
ExecutionClassLLMBacked ExecutionClass = "llm_backed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ChunkExecutionClassProvider interface {
|
||||||
|
ExecutionClass() ExecutionClass
|
||||||
|
}
|
||||||
|
|
||||||
type ValidationResult struct {
|
type ValidationResult struct {
|
||||||
Approved bool `json:"approved"`
|
Approved bool `json:"approved"`
|
||||||
ReasonCode string `json:"reason_code,omitempty"`
|
ReasonCode string `json:"reason_code,omitempty"`
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ type RunInput struct {
|
|||||||
|
|
||||||
type RunOutput struct {
|
type RunOutput struct {
|
||||||
Manifest artifacts.RunManifest `json:"manifest"`
|
Manifest artifacts.RunManifest `json:"manifest"`
|
||||||
|
ChunkPlan *artifacts.ChunkPlanSummary `json:"chunk_plan,omitempty"`
|
||||||
NormalizeOutputs []contracts.SerializedOutput `json:"normalize_outputs,omitempty"`
|
NormalizeOutputs []contracts.SerializedOutput `json:"normalize_outputs,omitempty"`
|
||||||
Rejected []contracts.RejectedOutput `json:"rejected,omitempty"`
|
Rejected []contracts.RejectedOutput `json:"rejected,omitempty"`
|
||||||
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
||||||
@@ -81,6 +82,11 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
|||||||
input.llmClient = input.Prepared.dependencies.LLM
|
input.llmClient = input.Prepared.dependencies.LLM
|
||||||
|
|
||||||
output.Manifest = manifestFromPipeline(input)
|
output.Manifest = manifestFromPipeline(input)
|
||||||
|
output.ChunkPlan = &artifacts.ChunkPlanSummary{
|
||||||
|
Mode: string(effectiveChunkCacheMode(input.ChunkCacheMode)), RequestedModule: input.pipeline.Chunk.Module,
|
||||||
|
LookupStatus: "skipped", LookupReason: "chunk plan lookup skipped",
|
||||||
|
ValidationStatus: "not_run", PublicationStatus: "not_requested",
|
||||||
|
}
|
||||||
checkpoints := input.Checkpoints
|
checkpoints := input.Checkpoints
|
||||||
if checkpoints == nil {
|
if checkpoints == nil {
|
||||||
checkpoints = NoopCheckpointRecorder()
|
checkpoints = NoopCheckpointRecorder()
|
||||||
@@ -193,6 +199,7 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
|||||||
return failOutput(output), fmt.Errorf("write chunk debug artifact: %w", err)
|
return failOutput(output), fmt.Errorf("write chunk debug artifact: %w", err)
|
||||||
}
|
}
|
||||||
chunkResult, err := r.runChunkPlan(ctx, input, doc, sourceInput, sessionID)
|
chunkResult, err := r.runChunkPlan(ctx, input, doc, sourceInput, sessionID)
|
||||||
|
applyChunkPlanExecution(&output, chunkResult)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failOutput(output), err
|
return failOutput(output), err
|
||||||
}
|
}
|
||||||
@@ -481,10 +488,14 @@ func manifestFromPipeline(input RunInput) artifacts.RunManifest {
|
|||||||
|
|
||||||
pipeline := input.pipeline
|
pipeline := input.pipeline
|
||||||
manifest := artifacts.RunManifest{
|
manifest := artifacts.RunManifest{
|
||||||
PipelineID: pipeline.ID,
|
PipelineID: pipeline.ID,
|
||||||
PipelineDigest: pipeline.Digest,
|
PipelineDigest: pipeline.Digest,
|
||||||
InputModule: pipeline.Input.Module,
|
InputModule: pipeline.Input.Module,
|
||||||
Chunker: pipeline.Chunk.Module,
|
Chunker: pipeline.Chunk.Module,
|
||||||
|
ChunkPlan: &artifacts.ChunkPlanManifest{
|
||||||
|
Mode: string(effectiveChunkCacheMode(input.ChunkCacheMode)),
|
||||||
|
RequestedModule: pipeline.Chunk.Module,
|
||||||
|
},
|
||||||
OutputEncoder: pipeline.Output.Module,
|
OutputEncoder: pipeline.Output.Module,
|
||||||
ArtifactLanes: make([]artifacts.ArtifactLaneManifest, 0, len(pipeline.ArtifactLanes)),
|
ArtifactLanes: make([]artifacts.ArtifactLaneManifest, 0, len(pipeline.ArtifactLanes)),
|
||||||
ValidatorChains: validatorChainManifests(pipeline.ValidatorChains),
|
ValidatorChains: validatorChainManifests(pipeline.ValidatorChains),
|
||||||
@@ -673,11 +684,38 @@ func cloneMetadata(metadata map[string]any) map[string]any {
|
|||||||
}
|
}
|
||||||
out := make(map[string]any, len(metadata))
|
out := make(map[string]any, len(metadata))
|
||||||
for key, value := range metadata {
|
for key, value := range metadata {
|
||||||
out[key] = value
|
out[key] = cloneMetadataValue(value)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cloneMetadataValue(value any) any {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
return cloneMetadata(typed)
|
||||||
|
case []any:
|
||||||
|
out := make([]any, len(typed))
|
||||||
|
for i := range typed {
|
||||||
|
out[i] = cloneMetadataValue(typed[i])
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
case json.RawMessage:
|
||||||
|
return append(json.RawMessage(nil), typed...)
|
||||||
|
case []byte:
|
||||||
|
return append([]byte(nil), typed...)
|
||||||
|
case []string:
|
||||||
|
return append([]string(nil), typed...)
|
||||||
|
case map[string]string:
|
||||||
|
out := make(map[string]string, len(typed))
|
||||||
|
for key, item := range typed {
|
||||||
|
out[key] = item
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func cloneLLMProfiles(profiles []artifacts.LLMProfileManifest) []artifacts.LLMProfileManifest {
|
func cloneLLMProfiles(profiles []artifacts.LLMProfileManifest) []artifacts.LLMProfileManifest {
|
||||||
if len(profiles) == 0 {
|
if len(profiles) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ type chunkPlanExecution struct {
|
|||||||
warnings []contracts.Warning
|
warnings []contracts.Warning
|
||||||
rejection *contracts.RejectedOutput
|
rejection *contracts.RejectedOutput
|
||||||
lookup ChunkPlanDecision
|
lookup ChunkPlanDecision
|
||||||
|
record *ChunkPlanRecord
|
||||||
|
action string
|
||||||
|
summary artifacts.ChunkPlanSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
func effectiveChunkCacheMode(mode ChunkCacheMode) ChunkCacheMode {
|
func effectiveChunkCacheMode(mode ChunkCacheMode) ChunkCacheMode {
|
||||||
@@ -33,33 +36,50 @@ func effectiveChunkCacheMode(mode ChunkCacheMode) ChunkCacheMode {
|
|||||||
func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.SourceDocument, sourceInput contracts.LLMInputMaterial, sessionID string) (chunkPlanExecution, error) {
|
func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.SourceDocument, sourceInput contracts.LLMInputMaterial, sessionID string) (chunkPlanExecution, error) {
|
||||||
mode := effectiveChunkCacheMode(input.ChunkCacheMode)
|
mode := effectiveChunkCacheMode(input.ChunkCacheMode)
|
||||||
chunker := input.Prepared.chunker
|
chunker := input.Prepared.chunker
|
||||||
result := chunkPlanExecution{lookup: ChunkPlanDecision{Reason: "chunk plan lookup skipped"}}
|
result := chunkPlanExecution{
|
||||||
|
lookup: ChunkPlanDecision{Reason: "chunk plan lookup skipped"},
|
||||||
|
summary: artifacts.ChunkPlanSummary{
|
||||||
|
Mode: string(mode), SourceDigest: doc.Digest, RequestedModule: chunker.Key(), LookupStatus: "skipped",
|
||||||
|
LookupReason: "chunk plan lookup skipped", ValidationStatus: "not_run", PublicationStatus: "not_requested",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if mode == ChunkCacheAuto {
|
if mode == ChunkCacheAuto {
|
||||||
record, decision, err := input.ChunkPlans.Load(doc.Digest)
|
record, decision, err := input.ChunkPlans.Load(doc.Digest)
|
||||||
result.lookup = decision
|
result.lookup = decision
|
||||||
|
result.summary.LookupStatus = string(decision.Status)
|
||||||
|
result.summary.LookupReason = decision.Reason
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
result.summary.LookupStatus = "skipped"
|
||||||
|
result.summary.LookupReason = "chunk plan lookup failed"
|
||||||
return result, fmt.Errorf("load chunk plan: %w", err)
|
return result, fmt.Errorf("load chunk plan: %w", err)
|
||||||
}
|
}
|
||||||
switch decision.Status {
|
switch decision.Status {
|
||||||
case ChunkPlanHit:
|
case ChunkPlanHit:
|
||||||
plan, chunks, validationErr := validateAndMaterializeChunkPlan(doc, record.Plan)
|
plan, chunks, validationErr := validateAndMaterializeChunkPlan(doc, record.Plan)
|
||||||
if validationErr == nil {
|
if validationErr == nil {
|
||||||
|
result.setCandidate(record, "reused")
|
||||||
validationWarnings, rejection, err := r.validateChunks(ctx, doc, chunker.Key(), chunks, sourceInput, sessionID, input.pipeline.ChunkReferences.ReferenceSet, input.Metadata, input.Prepared.chunkValidators, 1, input.Debug)
|
validationWarnings, rejection, err := r.validateChunks(ctx, doc, chunker.Key(), chunks, sourceInput, sessionID, input.pipeline.ChunkReferences.ReferenceSet, input.Metadata, input.Prepared.chunkValidators, 1, input.Debug)
|
||||||
result.plan = &plan
|
result.plan = &plan
|
||||||
result.chunks = chunks
|
result.chunks = chunks
|
||||||
result.warnings = append(cloneWarnings(record.Warnings), validationWarnings...)
|
result.warnings = append(cloneWarnings(record.Warnings), validationWarnings...)
|
||||||
result.rejection = rejection
|
result.rejection = rejection
|
||||||
result.accepted = rejection == nil && err == nil
|
result.accepted = rejection == nil && err == nil
|
||||||
|
result.setValidation(validationWarnings, rejection, err)
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
result.lookup = ChunkPlanDecision{Status: ChunkPlanInvalid, Reason: "stored chunk plan cannot be materialized against the current source"}
|
result.lookup = ChunkPlanDecision{Status: ChunkPlanInvalid, Reason: "stored chunk plan cannot be materialized against the current source"}
|
||||||
|
result.summary.LookupStatus = "invalid"
|
||||||
|
result.summary.LookupReason = result.lookup.Reason
|
||||||
case ChunkPlanMissing, ChunkPlanInvalid:
|
case ChunkPlanMissing, ChunkPlanInvalid:
|
||||||
// Generate below.
|
// Generate below.
|
||||||
default:
|
default:
|
||||||
return result, fmt.Errorf("load chunk plan returned unsupported status %q", decision.Status)
|
return result, fmt.Errorf("load chunk plan returned unsupported status %q", decision.Status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if mode == ChunkCacheAuto || mode == ChunkCacheRefresh {
|
||||||
|
result.summary.PublicationStatus = "not_published"
|
||||||
|
}
|
||||||
|
|
||||||
var producerWarnings []contracts.Warning
|
var producerWarnings []contracts.Warning
|
||||||
accepted, rejection, err := runWithRetry(ctx, input.pipeline.Chunk.Retries, func(attempt int) (bool, *contracts.RejectedOutput, error) {
|
accepted, rejection, err := runWithRetry(ctx, input.pipeline.Chunk.Retries, func(attempt int) (bool, *contracts.RejectedOutput, error) {
|
||||||
@@ -81,6 +101,33 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
|||||||
payload := map[string]any{"plan": debugChunkPlanEnvelope(chunkResult.Plan), "warnings": debugWarningEnvelopes(chunkResult.Warnings)}
|
payload := map[string]any{"plan": debugChunkPlanEnvelope(chunkResult.Plan), "warnings": debugWarningEnvelopes(chunkResult.Warnings)}
|
||||||
return false, nil, terminal.record(payload, attemptErr)
|
return false, nil, terminal.record(payload, attemptErr)
|
||||||
}
|
}
|
||||||
|
planDigest, digestErr := source.DigestChunkPlan(plan)
|
||||||
|
if digestErr != nil {
|
||||||
|
return false, nil, terminal.record(nil, fmt.Errorf("digest generated chunk plan: %w", digestErr))
|
||||||
|
}
|
||||||
|
producerMetadata, _ := moduleManifestMetadata(chunker)
|
||||||
|
profile := ""
|
||||||
|
if provider, ok := chunker.(contracts.ChunkExecutionClassProvider); ok && provider.ExecutionClass() == contracts.ExecutionClassLLMBacked {
|
||||||
|
profile = input.pipeline.Chunk.LLMProfile
|
||||||
|
}
|
||||||
|
candidate := ChunkPlanRecord{
|
||||||
|
SchemaVersion: ChunkPlanSchemaVersion, SourceDigest: doc.Digest, PlanDigest: planDigest,
|
||||||
|
Plan: source.CloneChunkPlan(plan),
|
||||||
|
Producer: ChunkPlanProducer{
|
||||||
|
InputModule: input.Prepared.input.Key(), ChunkModule: chunker.Key(), LLMProfile: profile,
|
||||||
|
References: append([]artifacts.ReferenceProvenance(nil), referenceTargetProvenance(input.pipeline.ChunkReferences)...),
|
||||||
|
Metadata: cloneMetadata(producerMetadata),
|
||||||
|
},
|
||||||
|
Warnings: cloneWarnings(chunkResult.Warnings), CreatedAt: time.Now().UTC(),
|
||||||
|
}
|
||||||
|
action := "generated"
|
||||||
|
if mode == ChunkCacheRefresh {
|
||||||
|
action = "refreshed"
|
||||||
|
}
|
||||||
|
if mode == ChunkCacheBypass {
|
||||||
|
action = "bypassed"
|
||||||
|
}
|
||||||
|
result.setCandidate(candidate, action)
|
||||||
validationWarnings, rejected, validationErr := r.validateChunks(attemptCtx, doc, chunker.Key(), chunks, sourceInput, sessionID, input.pipeline.ChunkReferences.ReferenceSet, input.Metadata, input.Prepared.chunkValidators, attempt, input.Debug)
|
validationWarnings, rejected, validationErr := r.validateChunks(attemptCtx, doc, chunker.Key(), chunks, sourceInput, sessionID, input.pipeline.ChunkReferences.ReferenceSet, input.Metadata, input.Prepared.chunkValidators, attempt, input.Debug)
|
||||||
attemptWarnings := append(cloneWarnings(chunkResult.Warnings), validationWarnings...)
|
attemptWarnings := append(cloneWarnings(chunkResult.Warnings), validationWarnings...)
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
@@ -88,18 +135,23 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
|||||||
"warnings": debugWarningEnvelopes(attemptWarnings), "rejection": debugRejectedOutputPtr(rejected),
|
"warnings": debugWarningEnvelopes(attemptWarnings), "rejection": debugRejectedOutputPtr(rejected),
|
||||||
}
|
}
|
||||||
if validationErr != nil || rejected != nil {
|
if validationErr != nil || rejected != nil {
|
||||||
|
result.setValidation(validationWarnings, rejected, validationErr)
|
||||||
return false, rejected, terminal.record(payload, validationErr)
|
return false, rejected, terminal.record(payload, validationErr)
|
||||||
}
|
}
|
||||||
result.chunks = chunks
|
result.chunks = chunks
|
||||||
result.plan = &plan
|
result.plan = &plan
|
||||||
result.warnings = attemptWarnings
|
result.warnings = attemptWarnings
|
||||||
producerWarnings = cloneWarnings(chunkResult.Warnings)
|
producerWarnings = cloneWarnings(chunkResult.Warnings)
|
||||||
|
result.setValidation(validationWarnings, nil, nil)
|
||||||
if debugErr := terminal.record(payload, nil); debugErr != nil {
|
if debugErr := terminal.record(payload, nil); debugErr != nil {
|
||||||
return false, nil, debugErr
|
return false, nil, debugErr
|
||||||
}
|
}
|
||||||
return true, nil, nil
|
return true, nil, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if result.summary.ValidationStatus == "not_run" {
|
||||||
|
result.summary.ValidationStatus = "error"
|
||||||
|
}
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
result.accepted = accepted
|
result.accepted = accepted
|
||||||
@@ -109,29 +161,69 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
if mode == ChunkCacheAuto || mode == ChunkCacheRefresh {
|
if mode == ChunkCacheAuto || mode == ChunkCacheRefresh {
|
||||||
planDigest, err := source.DigestChunkPlan(*result.plan)
|
record := cloneChunkPlanRecord(*result.record)
|
||||||
if err != nil {
|
record.Warnings = cloneWarnings(producerWarnings)
|
||||||
return result, fmt.Errorf("digest generated chunk plan: %w", err)
|
|
||||||
}
|
|
||||||
producerMetadata, _ := moduleManifestMetadata(chunker)
|
|
||||||
record := ChunkPlanRecord{
|
|
||||||
SchemaVersion: ChunkPlanSchemaVersion,
|
|
||||||
SourceDigest: doc.Digest,
|
|
||||||
PlanDigest: planDigest,
|
|
||||||
Plan: source.CloneChunkPlan(*result.plan),
|
|
||||||
Producer: ChunkPlanProducer{
|
|
||||||
InputModule: input.Prepared.input.Key(),
|
|
||||||
ChunkModule: chunker.Key(),
|
|
||||||
LLMProfile: input.pipeline.Chunk.LLMProfile,
|
|
||||||
References: append([]artifacts.ReferenceProvenance(nil), referenceTargetProvenance(input.pipeline.ChunkReferences)...),
|
|
||||||
Metadata: cloneMetadata(producerMetadata),
|
|
||||||
},
|
|
||||||
Warnings: cloneWarnings(producerWarnings),
|
|
||||||
CreatedAt: time.Now().UTC(),
|
|
||||||
}
|
|
||||||
if err := input.ChunkPlans.Save(record); err != nil {
|
if err := input.ChunkPlans.Save(record); err != nil {
|
||||||
return result, fmt.Errorf("save chunk plan: %w", err)
|
return result, fmt.Errorf("save chunk plan: %w", err)
|
||||||
}
|
}
|
||||||
|
result.summary.PublicationStatus = "published"
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (result *chunkPlanExecution) setCandidate(record ChunkPlanRecord, action string) {
|
||||||
|
cloned := cloneChunkPlanRecord(record)
|
||||||
|
result.record = &cloned
|
||||||
|
result.action = action
|
||||||
|
result.summary.Action = action
|
||||||
|
result.summary.SourceDigest = record.SourceDigest
|
||||||
|
result.summary.CandidateDigest = record.PlanDigest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (result *chunkPlanExecution) setValidation(warnings []contracts.Warning, rejection *contracts.RejectedOutput, err error) {
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
result.summary.ValidationStatus = "error"
|
||||||
|
case rejection != nil:
|
||||||
|
result.summary.ValidationStatus = "rejected"
|
||||||
|
case len(warnings) > 0:
|
||||||
|
result.summary.ValidationStatus = "approved_with_warnings"
|
||||||
|
default:
|
||||||
|
result.summary.ValidationStatus = "approved"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneChunkPlanRecord(record ChunkPlanRecord) ChunkPlanRecord {
|
||||||
|
record.Plan = source.CloneChunkPlan(record.Plan)
|
||||||
|
record.Producer.References = append([]artifacts.ReferenceProvenance(nil), record.Producer.References...)
|
||||||
|
record.Producer.Metadata = cloneMetadata(record.Producer.Metadata)
|
||||||
|
record.Warnings = cloneWarnings(record.Warnings)
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyChunkPlanExecution(output *RunOutput, result chunkPlanExecution) {
|
||||||
|
if output == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
summary := result.summary
|
||||||
|
output.ChunkPlan = &summary
|
||||||
|
if output.Manifest.ChunkPlan == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manifest := output.Manifest.ChunkPlan
|
||||||
|
manifest.Action = result.action
|
||||||
|
if result.record == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record := result.record
|
||||||
|
manifest.SourceDigest = record.SourceDigest
|
||||||
|
manifest.PlanDigest = record.PlanDigest
|
||||||
|
manifest.PlanSchemaVersion = record.SchemaVersion
|
||||||
|
manifest.ProducerInputModule = record.Producer.InputModule
|
||||||
|
manifest.ProducerModule = record.Producer.ChunkModule
|
||||||
|
manifest.ProducerLLMProfile = record.Producer.LLMProfile
|
||||||
|
manifest.ProducerReferences = append([]artifacts.ReferenceProvenance(nil), record.Producer.References...)
|
||||||
|
manifest.ProducerMetadata = cloneMetadata(record.Producer.Metadata)
|
||||||
|
createdAt := record.CreatedAt
|
||||||
|
manifest.CreatedAt = &createdAt
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,17 +54,30 @@ type manifestChunker struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c manifestChunker) ManifestMetadata() map[string]any { return c.metadata }
|
func (c manifestChunker) ManifestMetadata() map[string]any { return c.metadata }
|
||||||
|
func (manifestChunker) ExecutionClass() contracts.ExecutionClass {
|
||||||
|
return contracts.ExecutionClassLLMBacked
|
||||||
|
}
|
||||||
|
|
||||||
type llmCountingChunker struct {
|
type llmCountingChunker struct {
|
||||||
terminalChunker
|
terminalChunker
|
||||||
llmCalls *int
|
llmCalls *int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (llmCountingChunker) ExecutionClass() contracts.ExecutionClass {
|
||||||
|
return contracts.ExecutionClassLLMBacked
|
||||||
|
}
|
||||||
|
|
||||||
func (c llmCountingChunker) Plan(ctx context.Context, request contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
|
func (c llmCountingChunker) Plan(ctx context.Context, request contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
|
||||||
(*c.llmCalls)++
|
(*c.llmCalls)++
|
||||||
return c.terminalChunker.Plan(ctx, request)
|
return c.terminalChunker.Plan(ctx, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type deterministicChunker struct{ terminalChunker }
|
||||||
|
|
||||||
|
func (deterministicChunker) ExecutionClass() contracts.ExecutionClass {
|
||||||
|
return contracts.ExecutionClassDeterministic
|
||||||
|
}
|
||||||
|
|
||||||
type retryingChunker struct {
|
type retryingChunker struct {
|
||||||
key string
|
key string
|
||||||
plan source.ChunkPlan
|
plan source.ChunkPlan
|
||||||
@@ -93,19 +106,20 @@ func (l *dependencyLoader) Extract(_ string, _ string, dependencies []Checkpoint
|
|||||||
|
|
||||||
func TestRunnerChunkPlanModeMatrix(t *testing.T) {
|
func TestRunnerChunkPlanModeMatrix(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
mode ChunkCacheMode
|
mode ChunkCacheMode
|
||||||
decision ChunkPlanDecision
|
decision ChunkPlanDecision
|
||||||
wantLoads int
|
wantLoads int
|
||||||
wantSaves int
|
wantSaves int
|
||||||
wantCalls int
|
wantCalls int
|
||||||
|
wantAction string
|
||||||
}{
|
}{
|
||||||
{name: "auto hit", mode: ChunkCacheAuto, decision: ChunkPlanDecision{Status: ChunkPlanHit}, wantLoads: 1},
|
{name: "auto hit", mode: ChunkCacheAuto, decision: ChunkPlanDecision{Status: ChunkPlanHit}, wantLoads: 1, wantAction: "reused"},
|
||||||
{name: "auto missing", mode: ChunkCacheAuto, decision: ChunkPlanDecision{Status: ChunkPlanMissing}, wantLoads: 1, wantSaves: 1, wantCalls: 1},
|
{name: "auto missing", mode: ChunkCacheAuto, decision: ChunkPlanDecision{Status: ChunkPlanMissing}, wantLoads: 1, wantSaves: 1, wantCalls: 1, wantAction: "generated"},
|
||||||
{name: "auto invalid", mode: ChunkCacheAuto, decision: ChunkPlanDecision{Status: ChunkPlanInvalid}, wantLoads: 1, wantSaves: 1, wantCalls: 1},
|
{name: "auto invalid", mode: ChunkCacheAuto, decision: ChunkPlanDecision{Status: ChunkPlanInvalid}, wantLoads: 1, wantSaves: 1, wantCalls: 1, wantAction: "generated"},
|
||||||
{name: "bypass", mode: ChunkCacheBypass, wantCalls: 1},
|
{name: "bypass", mode: ChunkCacheBypass, wantCalls: 1, wantAction: "bypassed"},
|
||||||
{name: "empty bypass", wantCalls: 1},
|
{name: "empty bypass", wantCalls: 1, wantAction: "bypassed"},
|
||||||
{name: "refresh", mode: ChunkCacheRefresh, wantSaves: 1, wantCalls: 1},
|
{name: "refresh", mode: ChunkCacheRefresh, wantSaves: 1, wantCalls: 1, wantAction: "refreshed"},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@@ -114,16 +128,58 @@ func TestRunnerChunkPlanModeMatrix(t *testing.T) {
|
|||||||
llmCalls := 0
|
llmCalls := 0
|
||||||
prepared.chunker = llmCountingChunker{terminalChunker: terminalChunker{key: prepared.resolved.Chunk.Module, plan: plan, calls: &calls}, llmCalls: &llmCalls}
|
prepared.chunker = llmCountingChunker{terminalChunker: terminalChunker{key: prepared.resolved.Chunk.Module, plan: plan, calls: &calls}, llmCalls: &llmCalls}
|
||||||
store := &recordingChunkPlanStore{record: chunkPlanRecord(t, prepared, plan), decision: tc.decision}
|
store := &recordingChunkPlanStore{record: chunkPlanRecord(t, prepared, plan), decision: tc.decision}
|
||||||
if _, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), ChunkCacheMode: tc.mode, ChunkPlans: store}); err != nil {
|
output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), ChunkCacheMode: tc.mode, ChunkPlans: store})
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("Run() error = %v", err)
|
t.Fatalf("Run() error = %v", err)
|
||||||
}
|
}
|
||||||
if store.loads != tc.wantLoads || store.saves != tc.wantSaves || calls != tc.wantCalls {
|
if store.loads != tc.wantLoads || store.saves != tc.wantSaves || calls != tc.wantCalls {
|
||||||
t.Fatalf("calls = load %d save %d module %d, want %d %d %d", store.loads, store.saves, calls, tc.wantLoads, tc.wantSaves, tc.wantCalls)
|
t.Fatalf("calls = load %d save %d module %d, want %d %d %d", store.loads, store.saves, calls, tc.wantLoads, tc.wantSaves, tc.wantCalls)
|
||||||
}
|
}
|
||||||
|
if output.Manifest.ChunkPlan == nil || output.Manifest.ChunkPlan.Action != tc.wantAction || output.ChunkPlan == nil || output.ChunkPlan.Action != tc.wantAction {
|
||||||
|
t.Fatalf("chunk plan manifest = %#v summary = %#v, want action %q", output.Manifest.ChunkPlan, output.ChunkPlan, tc.wantAction)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunnerChunkPlanHitUsesStoredProducerProvenance(t *testing.T) {
|
||||||
|
prepared, plan := preparedTerminalDebugPipeline(t)
|
||||||
|
prepared.resolved.Chunk.Module = "chunk/requested"
|
||||||
|
prepared.resolved.Chunk.LLMProfile = "current-profile"
|
||||||
|
record := chunkPlanRecord(t, prepared, plan)
|
||||||
|
record.Producer.InputModule = "input/original"
|
||||||
|
record.Producer.ChunkModule = "chunk/original"
|
||||||
|
record.Producer.LLMProfile = "original-profile"
|
||||||
|
record.Producer.Metadata = map[string]any{"prompt": map[string]any{"id": "original"}}
|
||||||
|
store := &recordingChunkPlanStore{record: record, decision: ChunkPlanDecision{Status: ChunkPlanHit}}
|
||||||
|
output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), ChunkCacheMode: ChunkCacheAuto, ChunkPlans: store})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
manifest := output.Manifest.ChunkPlan
|
||||||
|
if manifest.RequestedModule != "chunk/requested" || manifest.ProducerModule != "chunk/original" || manifest.ProducerLLMProfile != "original-profile" {
|
||||||
|
t.Fatalf("chunk plan manifest = %#v", manifest)
|
||||||
|
}
|
||||||
|
storedNested := record.Producer.Metadata["prompt"].(map[string]any)
|
||||||
|
storedNested["id"] = "mutated"
|
||||||
|
if got := manifest.ProducerMetadata["prompt"].(map[string]any)["id"]; got != "original" {
|
||||||
|
t.Fatalf("producer metadata changed through stored alias: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunnerChunkPlanManifestRetainsCandidateOnRejection(t *testing.T) {
|
||||||
|
prepared, _ := preparedTerminalDebugPipeline(t)
|
||||||
|
validator := &countingChunkValidator{result: contracts.ValidationResult{Approved: false, ReasonCode: "policy", Message: "no"}}
|
||||||
|
prepared.chunkValidators.validators = []preparedValidator{{resolved: ResolvedValidator{Binding: Binding(validator.Name()), Target: ValidatorTargetChunk}, chunk: validator}}
|
||||||
|
output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), ChunkCacheMode: ChunkCacheRefresh, ChunkPlans: &recordingChunkPlanStore{}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if output.Manifest.ChunkPlan.Action != "refreshed" || output.Manifest.ChunkPlan.PlanDigest == "" || output.ChunkPlan.ValidationStatus != "rejected" || output.ChunkPlan.PublicationStatus != "not_published" {
|
||||||
|
t.Fatalf("manifest = %#v summary = %#v", output.Manifest.ChunkPlan, output.ChunkPlan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunnerValidatesChunkPlanPolicyInputs(t *testing.T) {
|
func TestRunnerValidatesChunkPlanPolicyInputs(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
@@ -328,6 +384,20 @@ func TestRunnerStoresProducerProvenanceAndProducerWarnings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunnerOmitsProducerProfileForDeterministicChunker(t *testing.T) {
|
||||||
|
prepared, plan := preparedTerminalDebugPipeline(t)
|
||||||
|
prepared.resolved.Chunk.LLMProfile = "configured-but-unused"
|
||||||
|
prepared.chunker = deterministicChunker{terminalChunker{key: prepared.resolved.Chunk.Module, plan: plan}}
|
||||||
|
store := &recordingChunkPlanStore{}
|
||||||
|
output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), ChunkCacheMode: ChunkCacheRefresh, ChunkPlans: store})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if store.saved.Producer.LLMProfile != "" || output.Manifest.ChunkPlan.ProducerLLMProfile != "" {
|
||||||
|
t.Fatalf("deterministic producer profile = stored %q manifest %q", store.saved.Producer.LLMProfile, output.Manifest.ChunkPlan.ProducerLLMProfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunnerRefreshChangesDownstreamChunkFingerprint(t *testing.T) {
|
func TestRunnerRefreshChangesDownstreamChunkFingerprint(t *testing.T) {
|
||||||
doc := typedTestDocumentWithUnits(2)
|
doc := typedTestDocumentWithUnits(2)
|
||||||
prepared := preparedConcurrentPipeline(t, 1)
|
prepared := preparedConcurrentPipeline(t, 1)
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ func (c *Chunker) Key() string {
|
|||||||
return Key
|
return Key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*Chunker) ExecutionClass() contracts.ExecutionClass {
|
||||||
|
return contracts.ExecutionClassLLMBacked
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Chunker) ReferenceSlots() []contracts.ReferenceSlot {
|
func (c *Chunker) ReferenceSlots() []contracts.ReferenceSlot {
|
||||||
return shared.ReferenceSlots(referenceSlotDescriptions)
|
return shared.ReferenceSlots(referenceSlotDescriptions)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ func (c *Chunker) Key() string {
|
|||||||
return Key
|
return Key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*Chunker) ExecutionClass() contracts.ExecutionClass {
|
||||||
|
return contracts.ExecutionClassDeterministic
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Chunker) ReferenceSlots() []contracts.ReferenceSlot {
|
func (c *Chunker) ReferenceSlots() []contracts.ReferenceSlot {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -273,11 +273,32 @@ func cloneMetadata(metadata map[string]any) map[string]any {
|
|||||||
}
|
}
|
||||||
out := make(map[string]any, len(metadata))
|
out := make(map[string]any, len(metadata))
|
||||||
for key, value := range metadata {
|
for key, value := range metadata {
|
||||||
out[key] = value
|
out[key] = cloneJSONMetadataValue(value)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cloneJSONMetadataValue(value any) any {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
return cloneMetadata(typed)
|
||||||
|
case []any:
|
||||||
|
out := make([]any, len(typed))
|
||||||
|
for i := range typed {
|
||||||
|
out[i] = cloneJSONMetadataValue(typed[i])
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
case stdjson.RawMessage:
|
||||||
|
return append(stdjson.RawMessage(nil), typed...)
|
||||||
|
case []byte:
|
||||||
|
return append([]byte(nil), typed...)
|
||||||
|
case []string:
|
||||||
|
return append([]string(nil), typed...)
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func encoderErrorf(format string, args ...any) error {
|
func encoderErrorf(format string, args ...any) error {
|
||||||
return fmt.Errorf("json output encoder: "+format, args...)
|
return fmt.Errorf("json output encoder: "+format, args...)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user