Verify prepared inputs from manifest evidence
This commit is contained in:
@@ -627,17 +627,26 @@ func resolveScriptoriumInput(inputCfg config.ScriptoriumInputConfig, execution a
|
||||
return analyzeInputFailure(describeErr)
|
||||
}
|
||||
if descriptor.Source.Kind == artifactpolicy.SourceKindStableInput {
|
||||
resolvedPath, ok, err := resolvePreparedStableInput(descriptor.Source.ID, execution.Paths)
|
||||
if err != nil {
|
||||
identity, err := artifacts.ResolvePreparedInput(execution.Paths, execution.Manifest, descriptor.Source.ID)
|
||||
if err == nil {
|
||||
return analyzeInputFound(identity.Path, nil)
|
||||
}
|
||||
if errors.Is(err, artifacts.ErrPreparedInputAbsent) {
|
||||
if inputCfg.Required {
|
||||
return analyzeInputFailure(err)
|
||||
return analyzeInputFailure(fmt.Errorf(
|
||||
"required prepared input source %q is unavailable; run narratio run-stage prepare %s --force",
|
||||
descriptor.Source.ID,
|
||||
execution.SessionID,
|
||||
))
|
||||
}
|
||||
return analyzeInputMissing()
|
||||
}
|
||||
if !ok {
|
||||
return analyzeInputMissing()
|
||||
}
|
||||
return analyzeInputFound(resolvedPath, nil)
|
||||
return analyzeInputFailure(fmt.Errorf(
|
||||
"prepared input source %q is invalid; run narratio run-stage prepare %s --force: %w",
|
||||
descriptor.Source.ID,
|
||||
execution.SessionID,
|
||||
err,
|
||||
))
|
||||
}
|
||||
if descriptor.Source.Kind == artifactpolicy.SourceKindPreviousArtifact {
|
||||
resolved, err := artifacts.ResolvePreviousSessionArtifactWithCatalog(execution.Paths, execution.Manifest, source, execution.Catalog)
|
||||
@@ -710,28 +719,6 @@ func requiredBuiltInInputError(source string, execution analyzeExecutionContext)
|
||||
)
|
||||
}
|
||||
|
||||
func resolvePreparedStableInput(sourceID string, paths artifacts.SessionPaths) (string, bool, error) {
|
||||
filename, ok := preparedStableInputFilename(sourceID)
|
||||
if !ok {
|
||||
return "", false, fmt.Errorf("unsupported prepared input source %q", sourceID)
|
||||
}
|
||||
path := filepath.Join(paths.InputsDir, filename)
|
||||
if err := requireNonEmptyFile(path, "prepared input "+sourceID); err != nil {
|
||||
return "", false, fmt.Errorf(
|
||||
"prepared input source %q is unavailable; run narratio run-stage prepare %s --force: %w",
|
||||
sourceID,
|
||||
paths.SessionID,
|
||||
err,
|
||||
)
|
||||
}
|
||||
return path, true, nil
|
||||
}
|
||||
|
||||
func preparedStableInputFilename(sourceID string) (string, bool) {
|
||||
descriptor, ok := artifactpolicy.DescribePreparedInputSource(sourceID)
|
||||
return descriptor.Filename, ok
|
||||
}
|
||||
|
||||
func buildAnalyzeRuntimeArtifactCatalog(
|
||||
paths artifacts.SessionPaths,
|
||||
m *manifest.Manifest,
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/scriptorium"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
@@ -1099,9 +1100,11 @@ func TestAnalyzeResolvesPreparedStableInputSources(t *testing.T) {
|
||||
playersPath := filepath.Join(paths.InputsDir, "players.yml")
|
||||
partyPath := filepath.Join(paths.InputsDir, "party.yml")
|
||||
glossaryPath := filepath.Join(paths.InputsDir, "glossary.yml")
|
||||
writeAnalyzeFile(t, playersPath, "- Eric\n")
|
||||
writeAnalyzeFile(t, partyPath, "- Arannis\n")
|
||||
writeAnalyzeFile(t, glossaryPath, "- term: Ten Towns\n")
|
||||
spellCatalogPath := filepath.Join(paths.InputsDir, "spell_catalog.json")
|
||||
recordPreparedAnalyzeInput(t, m, artifactpolicy.SourceInputPlayers, playersPath, "- Eric\n")
|
||||
recordPreparedAnalyzeInput(t, m, artifactpolicy.SourceInputParty, partyPath, "- Arannis\n")
|
||||
recordPreparedAnalyzeInput(t, m, artifactpolicy.SourceInputGlossary, glossaryPath, "- term: Ten Towns\n")
|
||||
recordPreparedAnalyzeInput(t, m, artifactpolicy.SourceInputSpellCatalog, spellCatalogPath, "{\"spells\":[]}\n")
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["players"] = config.ScriptoriumInputConfig{
|
||||
@@ -1116,6 +1119,10 @@ func TestAnalyzeResolvesPreparedStableInputSources(t *testing.T) {
|
||||
Source: "narratio.input.glossary",
|
||||
Required: true,
|
||||
}
|
||||
artifact.Inputs["spells"] = config.ScriptoriumInputConfig{
|
||||
Source: "narratio.input.spell_catalog",
|
||||
Required: true,
|
||||
}
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
|
||||
|
||||
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||
@@ -1134,6 +1141,9 @@ func TestAnalyzeResolvesPreparedStableInputSources(t *testing.T) {
|
||||
if fake.RunRequests[0].InputPaths["glossary"] != glossaryPath {
|
||||
t.Fatalf("glossary input = %q, want %q", fake.RunRequests[0].InputPaths["glossary"], glossaryPath)
|
||||
}
|
||||
if fake.RunRequests[0].InputPaths["spells"] != spellCatalogPath {
|
||||
t.Fatalf("spells input = %q, want %q", fake.RunRequests[0].InputPaths["spells"], spellCatalogPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeMissingRequiredPreparedStableInputFailsWithPrepareGuidance(t *testing.T) {
|
||||
@@ -1182,6 +1192,28 @@ func TestAnalyzeMissingOptionalPreparedStableInputIsOmitted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeInvalidOptionalPreparedStableInputFailsWithPrepareGuidance(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
playersPath := filepath.Join(paths.InputsDir, "players.yml")
|
||||
recordPreparedAnalyzeInput(t, m, artifactpolicy.SourceInputPlayers, playersPath, "- Eric\n")
|
||||
m.Inputs[len(m.Inputs)-1].Checksum = strings.Repeat("0", 64)
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["players"] = config.ScriptoriumInputConfig{
|
||||
Source: "narratio.input.players",
|
||||
Required: false,
|
||||
}
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
|
||||
|
||||
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||
if err == nil || !strings.Contains(err.Error(), "prepared input source \"narratio.input.players\" is invalid") ||
|
||||
!strings.Contains(err.Error(), "run narratio run-stage prepare 2026-05-03 --force") {
|
||||
t.Fatalf("Run() error = %v, want invalid prepared input guidance", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeSupportsCanonicalNormalizedTranscriptSourceFromManifestOutput(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
@@ -1650,6 +1682,24 @@ func writeAnalyzeFile(t *testing.T, path, contents string) {
|
||||
}
|
||||
}
|
||||
|
||||
func recordPreparedAnalyzeInput(t *testing.T, m *manifest.Manifest, sourceID, path, contents string) {
|
||||
t.Helper()
|
||||
writeAnalyzeFile(t, path, contents)
|
||||
descriptor, ok := artifactpolicy.DescribePreparedInputSource(sourceID)
|
||||
if !ok {
|
||||
t.Fatalf("DescribePreparedInputSource(%q) ok = false", sourceID)
|
||||
}
|
||||
checksum, err := artifacts.SHA256File(path)
|
||||
if err != nil {
|
||||
t.Fatalf("SHA256File(%q) error = %v", path, err)
|
||||
}
|
||||
m.Inputs = append(m.Inputs, manifest.InputRecord{
|
||||
Kind: descriptor.ManifestKind,
|
||||
Path: path,
|
||||
Checksum: checksum,
|
||||
})
|
||||
}
|
||||
|
||||
func writeAnalyzeFileNoTest(path, contents string) {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user