Complete compatibility purge audit

This commit is contained in:
2026-09-02 14:18:51 +00:00
parent 57719b7d81
commit e9c4873030
19 changed files with 99 additions and 160 deletions

View File

@@ -140,7 +140,8 @@ Extraction sources (`narratio.extraction.*`):
Previous-session sources (`narratio.previous_session.artifact.*`): Previous-session sources (`narratio.previous_session.artifact.*`):
- resolve only from local `previous/` cache state; - resolve only from one matching `previous_artifact` input record in the
current manifest, including its exact source ID and checksum;
- are hydrated only from the unique `published_output` declaration for their - are hydrated only from the unique `published_output` declaration for their
canonical source in the selected remote commit; and canonical source in the selected remote commit; and
- never infer a path from a previous manifest, current configuration, basename, - never infer a path from a previous manifest, current configuration, basename,
@@ -202,10 +203,9 @@ Core helpers:
- `ValidateCurrentStateIdentity` - `ValidateCurrentStateIdentity`
- `RemoteCommitManifest` and `CurrentCommitPointer` - `RemoteCommitManifest` and `CurrentCommitPointer`
Typed missing-state errors: Typed missing-state error:
- `CurrentRunPointerMissingError` (`ErrCurrentRunPointerMissing`) - `CurrentCommitPointerMissingError` (`ErrCurrentCommitPointerMissing`)
- `CurrentManifestMissingError` (`ErrCurrentManifestMissing`)
Identity validation supports caller-provided expectations: Identity validation supports caller-provided expectations:
@@ -262,7 +262,6 @@ physical layout.
`internal/artifacts/prepared_input.go` `internal/artifacts/prepared_input.go`
- Current state: `internal/artifacts/current_state.go`, - Current state: `internal/artifacts/current_state.go`,
`internal/artifacts/current_state_commit.go`, `internal/artifacts/current_state_commit.go`,
`internal/artifacts/current_state_legacy.go`
- Paths and keys: `internal/artifacts/paths.go`, - Paths and keys: `internal/artifacts/paths.go`,
`internal/artifacts/s3_keys.go` `internal/artifacts/s3_keys.go`
- Previous requirements: `internal/artifacts/previous_requirements.go` - Previous requirements: `internal/artifacts/previous_requirements.go`

View File

@@ -315,7 +315,6 @@ where a durable running record can require operator interpretation.
`internal/manifest/run_manifest.go` `internal/manifest/run_manifest.go`
- Remote commit model and readers: `internal/artifacts/remote_commit.go`, - Remote commit model and readers: `internal/artifacts/remote_commit.go`,
`internal/artifacts/current_state_commit.go`, `internal/artifacts/current_state_commit.go`,
`internal/artifacts/current_state_legacy.go`
- Persistence and validation: `internal/manifest/store.go` - Persistence and validation: `internal/manifest/store.go`
- Package tests: `internal/manifest/*_test.go` - Package tests: `internal/manifest/*_test.go`
- Assembled execution behavior: `internal/app/runner_test.go`, - Assembled execution behavior: `internal/app/runner_test.go`,

View File

@@ -15,7 +15,7 @@ Normalize polished transcript into final transcript using Seriatim.
## Key Behavior ## Key Behavior
- resolves polished transcript from manifest outputs/canonical fallback. - resolves polished transcript only from verified manifest output evidence.
- applies `pipeline.normalize` config or default normalize config. - applies `pipeline.normalize` config or default normalize config.
- runs Seriatim normalize with configured timeout/binary. - runs Seriatim normalize with configured timeout/binary.
- validates normalized transcript and optional report. - validates normalized transcript and optional report.

View File

@@ -16,7 +16,7 @@ Run Audita polishing on base transcript and produce polished transcript.
## Key Behavior ## Key Behavior
- resolves base transcript from merge outputs/canonical fallback. - resolves base transcript only from verified merge manifest output evidence.
- invokes an Audita runner configured with static model/runtime options; the - invokes an Audita runner configured with static model/runtime options; the
invocation supplies paths and modules. invocation supplies paths and modules.
- validates processed transcript structure (`segments` array required). - validates processed transcript structure (`segments` array required).

View File

@@ -21,7 +21,7 @@ records that may consume rendered transcripts.
## Key Behavior ## Key Behavior
- uses `pipeline.render` settings (enabled/format/title/booleans). - uses `pipeline.render` settings (enabled/format/title/booleans).
- resolves inputs manifest-first, then canonical fallback. - resolves inputs only from verified manifest output evidence.
- writes run-local outputs first, then materializes canonical session outputs. - writes run-local outputs first, then materializes canonical session outputs.
- records input provenance, output paths, adapter metadata, logs, and generated config refs. - records input provenance, output paths, adapter metadata, logs, and generated config refs.
- when `pipeline.render.enabled=false`, completes successfully with no outputs - when `pipeline.render.enabled=false`, completes successfully with no outputs

View File

@@ -373,7 +373,7 @@ consumers require current manifest evidence.
## Stage 7 — Complete The Purge And Validate The Repository ## Stage 7 — Complete The Purge And Validate The Repository
**Status: Pending** **Status: Completed**
### Goal ### Goal

View File

@@ -150,7 +150,7 @@ keys remain protected without treating ordinary D&D data as private.
### Workstream 3 — Canonical contracts and compatibility purge ### Workstream 3 — Canonical contracts and compatibility purge
**Status:** Pending **Status:** Complete
**Audit sources:** H43 and C01. **Audit sources:** H43 and C01.

View File

@@ -12,9 +12,9 @@ import (
func TestDownloadObjectToTempSuccess(t *testing.T) { func TestDownloadObjectToTempSuccess(t *testing.T) {
store := &FakeBackend{} store := &FakeBackend{}
store.SeedObject(FakeObject{Key: "sessions/a/current/run_id.txt", Data: []byte("run-123\n")}) store.SeedObject(FakeObject{Key: "sessions/a/runs/run-123/metadata.txt", Data: []byte("run-123\n")})
path, err := DownloadObjectToTemp(context.Background(), store, "sessions/a/current/run_id.txt", "narratio-test-*.txt") path, err := DownloadObjectToTemp(context.Background(), store, "sessions/a/runs/run-123/metadata.txt", "narratio-test-*.txt")
if err != nil { if err != nil {
t.Fatalf("DownloadObjectToTemp() error = %v", err) t.Fatalf("DownloadObjectToTemp() error = %v", err)
} }
@@ -38,7 +38,7 @@ func TestDownloadObjectToTempFailedDownloadRemovesTempFile(t *testing.T) {
t.Fatalf("Glob(before) error = %v", err) t.Fatalf("Glob(before) error = %v", err)
} }
path, err := DownloadObjectToTemp(context.Background(), store, "sessions/a/current/run_id.txt", pattern) path, err := DownloadObjectToTemp(context.Background(), store, "sessions/a/runs/run-123/metadata.txt", pattern)
if !errors.Is(err, sentinel) { if !errors.Is(err, sentinel) {
t.Fatalf("DownloadObjectToTemp() error = %v, want %v", err, sentinel) t.Fatalf("DownloadObjectToTemp() error = %v, want %v", err, sentinel)
} }
@@ -58,7 +58,7 @@ func TestDownloadObjectToTempCallerContextWrappingPreservesCause(t *testing.T) {
sentinel := errors.New("object missing") sentinel := errors.New("object missing")
store := &FakeBackend{DownloadErr: sentinel} store := &FakeBackend{DownloadErr: sentinel}
_, err := DownloadObjectToTemp(context.Background(), store, "sessions/a/current/run_id.txt", "narratio-test-*.txt") _, err := DownloadObjectToTemp(context.Background(), store, "sessions/a/runs/run-123/metadata.txt", "narratio-test-*.txt")
if err == nil { if err == nil {
t.Fatal("DownloadObjectToTemp() error = nil, want error") t.Fatal("DownloadObjectToTemp() error = nil, want error")
} }

View File

@@ -5,6 +5,7 @@ import (
"testing" "testing"
"time" "time"
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/manifest" "gitea.maximumdirect.net/eric/narratio/internal/manifest"
) )
@@ -29,3 +30,14 @@ func seedCurrentBuiltInEvidence(t *testing.T, manifestPath, sessionID, campaign,
t.Fatal(err) t.Fatal(err)
} }
} }
func setCurrentPreviousArtifactInput(t *testing.T, m *manifest.Manifest, path string) {
t.Helper()
checksum, err := artifacts.SHA256File(path)
if err != nil {
t.Fatal(err)
}
m.Inputs = append(m.Inputs, manifest.InputRecord{
Kind: "previous_artifact", Source: artifactpolicy.PreviousSessionSourceID("session_recap"), Path: path, Checksum: checksum,
})
}

View File

@@ -64,7 +64,7 @@ func TestExecuteRestoreRecognizedAndReturnsNYI(t *testing.T) {
{ {
Kind: RestoreActionDownload, Kind: RestoreActionDownload,
LocalRelativePath: "manifest.json", LocalRelativePath: "manifest.json",
RemoteKey: "dnd/campaigns/sample-campaign/sessions/2026-05-03/current/manifest.json", RemoteKey: "dnd/campaigns/sample-campaign/sessions/2026-05-03/runs/20260519T010203Z-a1b2c3d4/session-manifest.json",
Reason: "local file missing", Reason: "local file missing",
}, },
}, },
@@ -165,7 +165,7 @@ func TestExecuteRestoreDiscoveryErrorSurfaced(t *testing.T) {
return &storage.FakeBackend{}, nil return &storage.FakeBackend{}, nil
} }
discoverRemoteCurrentStateFn = func(context.Context, *config.Config, storage.ObjectStore) (*RemoteCurrentState, error) { discoverRemoteCurrentStateFn = func(context.Context, *config.Config, storage.ObjectStore) (*RemoteCurrentState, error) {
return nil, fmt.Errorf("remote current run pointer missing: %q", "dnd/campaigns/sample-campaign/sessions/2026-05-03/current/run_id.txt") return nil, fmt.Errorf("remote current commit pointer missing: %q", "dnd/campaigns/sample-campaign/sessions/2026-05-03/current/commit-pointer.json")
} }
workspaceRoot := t.TempDir() workspaceRoot := t.TempDir()
@@ -177,7 +177,7 @@ func TestExecuteRestoreDiscoveryErrorSurfaced(t *testing.T) {
if code == 0 { if code == 0 {
t.Fatal("exit code = 0, want non-zero") t.Fatal("exit code = 0, want non-zero")
} }
if !strings.Contains(stderr.String(), "remote current run pointer missing") { if !strings.Contains(stderr.String(), "remote current commit pointer missing") {
t.Fatalf("stderr = %q, want discovery error context", stderr.String()) t.Fatalf("stderr = %q, want discovery error context", stderr.String())
} }
} }
@@ -250,7 +250,7 @@ func TestExecuteRestoreLoadsSecretsBeforeObjectStoreInit(t *testing.T) {
{ {
Kind: RestoreActionDownload, Kind: RestoreActionDownload,
LocalRelativePath: "manifest.json", LocalRelativePath: "manifest.json",
RemoteKey: "dnd/campaigns/sample-campaign/sessions/2026-05-03/current/manifest.json", RemoteKey: "dnd/campaigns/sample-campaign/sessions/2026-05-03/runs/20260519T010203Z-a1b2c3d4/session-manifest.json",
Reason: "local file missing", Reason: "local file missing",
}, },
}, },

View File

@@ -211,6 +211,7 @@ previous_session_id: 2026-04-26
t.Fatalf("load restored manifest: %v", err) t.Fatalf("load restored manifest: %v", err)
} }
setCurrentBuiltInEvidence(t, localManifest, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, filepath.Join(sessionRoot, "transcripts", "final.trimmed.json")) setCurrentBuiltInEvidence(t, localManifest, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, filepath.Join(sessionRoot, "transcripts", "final.trimmed.json"))
setCurrentPreviousArtifactInput(t, localManifest, filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md"))
if err := (&manifest.LocalStore{}).Save(context.Background(), filepath.Join(sessionRoot, "manifest.json"), localManifest); err != nil { if err := (&manifest.LocalStore{}).Save(context.Background(), filepath.Join(sessionRoot, "manifest.json"), localManifest); err != nil {
t.Fatalf("save manifest evidence: %v", err) t.Fatalf("save manifest evidence: %v", err)
} }

View File

@@ -29,7 +29,6 @@ const (
ArtifactBoundsSession = "narratio.bounds.session" ArtifactBoundsSession = "narratio.bounds.session"
ArtifactProvenancePreviousCacheManifestInput = "manifest.inputs.previous_cache" ArtifactProvenancePreviousCacheManifestInput = "manifest.inputs.previous_cache"
ArtifactProvenancePreviousCacheFilesystem = "current_session.previous_cache"
) )
const ( const (
@@ -264,54 +263,49 @@ func ResolvePreviousSessionArtifactWithCatalog(
return ResolvedSessionArtifact{}, fmt.Errorf("previous-session artifact source %q requires runtime artifact catalog", source) return ResolvedSessionArtifact{}, fmt.Errorf("previous-session artifact source %q requires runtime artifact catalog", source)
} }
configuredSourceID := ConfiguredArtifactSourceID(artifactName) if _, ok := catalog.Lookup(ConfiguredArtifactSourceID(artifactName)); !ok {
entry, ok := catalog.Lookup(configuredSourceID)
if !ok {
return ResolvedSessionArtifact{}, fmt.Errorf("unsupported previous-session artifact source %q", source) return ResolvedSessionArtifact{}, fmt.Errorf("unsupported previous-session artifact source %q", source)
} }
candidates := previousSessionCacheCandidatePaths(paths, entry.CanonicalRelPath) if m == nil {
if len(candidates) == 0 {
return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: source} return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: source}
} }
var selected manifest.InputRecord
manifestInputPaths := manifestInputPathSet(paths, m) count := 0
fallback := "" for _, input := range m.Inputs {
for _, candidate := range candidates { if input.Kind != "previous_artifact" || input.Source != source {
exists, isDir, statErr := pathExists(candidate)
if statErr != nil {
return ResolvedSessionArtifact{}, fmt.Errorf("stat %q: %w", candidate, statErr)
}
if !exists || isDir {
continue continue
} }
if err := validateResolvedContent(candidate, contentText); err != nil { candidate := filepath.Clean(ResolveSessionLocalPathForRead(paths, input.Path))
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: %w", source, err) if strings.TrimSpace(candidate) == "" {
continue
} }
if _, ok := manifestInputPaths[candidate]; ok { if _, err := pathsafe.SlashRelativeFromRoot(paths.Root, candidate); err != nil {
return ResolvedSessionArtifact{ continue
ID: source,
Path: candidate,
ProducerStage: "prepare",
OutputKind: "previous_session_cache",
Provenance: ArtifactProvenancePreviousCacheManifestInput,
}, nil
}
if fallback == "" {
fallback = candidate
} }
selected, count = input, count+1
} }
if count != 1 {
if fallback != "" { return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: source}
return ResolvedSessionArtifact{
ID: source,
Path: fallback,
ProducerStage: "prepare",
OutputKind: "previous_session_cache",
Provenance: ArtifactProvenancePreviousCacheFilesystem,
}, nil
} }
path := filepath.Clean(ResolveSessionLocalPathForRead(paths, selected.Path))
return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: source} if err := requireSafeSessionRegularFile(paths.Root, path); err != nil {
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: unsafe previous-session artifact: %w", source, err)
}
if err := validateResolvedContent(path, contentText); err != nil {
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: %w", source, err)
}
checksum := strings.TrimSpace(selected.Checksum)
if len(checksum) != 64 || strings.ToLower(checksum) != checksum {
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: previous-session artifact checksum is invalid", source)
}
actualChecksum, err := SHA256File(path)
if err != nil {
return ResolvedSessionArtifact{}, fmt.Errorf("checksum %q: %w", source, err)
}
if actualChecksum != checksum {
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: checksum mismatch", source)
}
return ResolvedSessionArtifact{ID: source, Path: path, ProducerStage: "prepare", OutputKind: "previous_session_cache", Provenance: ArtifactProvenancePreviousCacheManifestInput, Checksum: checksum}, nil
} }
func manifestArtifactCandidate(paths SessionPaths, m *manifest.Manifest, spec artifactSpec) (ResolvedSessionArtifact, error) { func manifestArtifactCandidate(paths SessionPaths, m *manifest.Manifest, spec artifactSpec) (ResolvedSessionArtifact, error) {
@@ -384,65 +378,6 @@ func requireSafeSessionRegularFile(root, path string) error {
return nil return nil
} }
func previousSessionCacheCandidatePaths(paths SessionPaths, canonicalRelPath string) []string {
trimmed := strings.TrimSpace(canonicalRelPath)
if trimmed == "" {
return nil
}
normalized := filepath.ToSlash(filepath.Clean(filepath.FromSlash(trimmed)))
if normalized == "." || normalized == "" || normalized == ".." || strings.HasPrefix(normalized, "../") || strings.HasPrefix(normalized, "/") {
return nil
}
relCandidates := []string{normalized}
const artifactsPrefix = "artifacts/"
if strings.HasPrefix(normalized, artifactsPrefix) && len(normalized) > len(artifactsPrefix) {
relCandidates = append(relCandidates, strings.TrimPrefix(normalized, artifactsPrefix))
}
out := make([]string, 0, len(relCandidates))
seen := map[string]struct{}{}
for _, rel := range relCandidates {
abs, err := SessionPreviousArtifactPath(paths, rel)
if err != nil {
continue
}
abs = filepath.Clean(abs)
if _, ok := seen[abs]; ok {
continue
}
seen[abs] = struct{}{}
out = append(out, abs)
}
return out
}
func manifestInputPathSet(paths SessionPaths, m *manifest.Manifest) map[string]struct{} {
if m == nil || len(m.Inputs) == 0 {
return nil
}
out := make(map[string]struct{}, len(m.Inputs))
for _, in := range m.Inputs {
resolved := filepath.Clean(ResolveSessionLocalPathForRead(paths, in.Path))
if strings.TrimSpace(resolved) == "" {
continue
}
out[resolved] = struct{}{}
}
return out
}
func pathExists(path string) (exists bool, isDir bool, err error) {
info, err := os.Stat(path)
if err == nil {
return true, info.IsDir(), nil
}
if errors.Is(err, os.ErrNotExist) {
return false, false, nil
}
return false, false, err
}
func validateResolvedContent(path string, kind artifactContentKind) error { func validateResolvedContent(path string, kind artifactContentKind) error {
switch kind { switch kind {
case contentTranscriptJSON: case contentTranscriptJSON:

View File

@@ -4,11 +4,11 @@ import (
"errors" "errors"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
"gitea.maximumdirect.net/eric/narratio/internal/manifest" "gitea.maximumdirect.net/eric/narratio/internal/manifest"
) )
@@ -474,9 +474,13 @@ func TestResolvePreviousSessionArtifactWithCatalogPrefersManifestInputRecord(t *
} }
m := manifest.New("session", time.Now().UTC()) m := manifest.New("session", time.Now().UTC())
m.Inputs = []manifest.InputRecord{ checksum, err := SHA256File(manifestBackedPath)
{Kind: "previous_artifact", Path: manifestBackedPath}, if err != nil {
t.Fatal(err)
} }
m.Inputs = []manifest.InputRecord{{
Kind: "previous_artifact", Source: artifactpolicy.PreviousSessionSourceID("session_recap"), Path: manifestBackedPath, Checksum: checksum,
}}
resolved, err := ResolvePreviousSessionArtifactWithCatalog( resolved, err := ResolvePreviousSessionArtifactWithCatalog(
paths, paths,
@@ -495,7 +499,7 @@ func TestResolvePreviousSessionArtifactWithCatalogPrefersManifestInputRecord(t *
} }
} }
func TestResolvePreviousSessionArtifactWithCatalogFallsBackToPreparedCachePath(t *testing.T) { func TestResolvePreviousSessionArtifactWithCatalogRejectsIncidentalPreparedCachePath(t *testing.T) {
workspace := t.TempDir() workspace := t.TempDir()
paths := buildSessionPaths(workspace, "campaign", "session") paths := buildSessionPaths(workspace, "campaign", "session")
@@ -517,31 +521,14 @@ func TestResolvePreviousSessionArtifactWithCatalogFallsBackToPreparedCachePath(t
t.Fatalf("RegisterConfiguredArtifacts() error = %v", err) t.Fatalf("RegisterConfiguredArtifacts() error = %v", err)
} }
resolved, err := ResolvePreviousSessionArtifactWithCatalog( _, err := ResolvePreviousSessionArtifactWithCatalog(
paths, paths,
nil, nil,
"narratio.previous_session.artifact.session_recap", "narratio.previous_session.artifact.session_recap",
catalog, catalog,
) )
if err != nil { if !errors.Is(err, ErrSessionArtifactNotFound) {
t.Fatalf("ResolvePreviousSessionArtifactWithCatalog() error = %v", err) t.Fatalf("ResolvePreviousSessionArtifactWithCatalog() error = %v, want unavailable incidental cache", err)
}
if resolved.Path != fallbackPath {
t.Fatalf("resolved path = %q, want %q", resolved.Path, fallbackPath)
}
if resolved.Provenance != ArtifactProvenancePreviousCacheFilesystem {
t.Fatalf("provenance = %q, want %q", resolved.Provenance, ArtifactProvenancePreviousCacheFilesystem)
}
}
func TestPreviousSessionCacheCandidatePathsStripsArtifactsPrefix(t *testing.T) {
workspace := t.TempDir()
paths := buildSessionPaths(workspace, "campaign", "session")
got := previousSessionCacheCandidatePaths(paths, "artifacts/session_recap.md")
want := []string{filepath.Join(paths.PreviousArtifactsDir, "session_recap.md")}
if !reflect.DeepEqual(got, want) {
t.Fatalf("previousSessionCacheCandidatePaths() = %#v, want %#v", got, want)
} }
} }

View File

@@ -7,6 +7,7 @@ import (
"time" "time"
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel" "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/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/manifest" "gitea.maximumdirect.net/eric/narratio/internal/manifest"
) )
@@ -55,3 +56,14 @@ func setCurrentBuiltInEvidence(t *testing.T, m *manifest.Manifest, stage, kind,
ProducerRunID: stage + "-run", Checksum: checksum, ProducerRunID: stage + "-run", Checksum: checksum,
}}) }})
} }
func setCurrentPreviousArtifactInput(t *testing.T, m *manifest.Manifest, path string) {
t.Helper()
checksum, err := artifacts.SHA256File(path)
if err != nil {
t.Fatal(err)
}
m.Inputs = append(m.Inputs, manifest.InputRecord{
Kind: "previous_artifact", Source: artifactpolicy.PreviousSessionSourceID("session_recap"), Path: path, Checksum: checksum,
})
}

View File

@@ -61,7 +61,7 @@ func TestResolveAnalyzeInputIdentitiesSupportsEverySourceKind(t *testing.T) {
setup: func(t *testing.T, env *Env, m *manifest.Manifest) string { setup: func(t *testing.T, env *Env, m *manifest.Manifest) string {
path := mustPreviousArtifactPath(t, sessionPathsForEnv(env, m.SessionID), "artifacts/session_recap.md") path := mustPreviousArtifactPath(t, sessionPathsForEnv(env, m.SessionID), "artifacts/session_recap.md")
writeAnalyzeFile(t, path, "previous recap\n") writeAnalyzeFile(t, path, "previous recap\n")
m.Inputs = append(m.Inputs, manifest.InputRecord{Kind: "previous_artifact", Path: path}) setCurrentPreviousArtifactInput(t, m, path)
return path return path
}, },
wantOutputKind: "previous_session_cache", wantOutputKind: "previous_session_cache",

View File

@@ -392,6 +392,7 @@ func TestAnalyzeIncludesCanonicalPreviousRecapWhenPreparedCacheExists(t *testing
previousRecapPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md") previousRecapPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md")
writeAnalyzeFile(t, previousRecapPath, "previous recap\n") writeAnalyzeFile(t, previousRecapPath, "previous recap\n")
setCurrentPreviousArtifactInput(t, m, previousRecapPath)
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{ env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{
Enabled: true, Enabled: true,
@@ -804,10 +805,7 @@ func TestAnalyzeResolvesCanonicalPreviousSessionArtifactFromManifestInput(t *tes
previousPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md") previousPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md")
writeAnalyzeFile(t, previousPath, "previous recap\n") writeAnalyzeFile(t, previousPath, "previous recap\n")
m.Inputs = append(m.Inputs, manifest.InputRecord{ setCurrentPreviousArtifactInput(t, m, previousPath)
Kind: "previous_artifact",
Path: previousPath,
})
sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{ sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{
@@ -860,10 +858,7 @@ func TestAnalyzeRenderDebugWithCanonicalPreviousSessionInput(t *testing.T) {
previousPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md") previousPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md")
writeAnalyzeFile(t, previousPath, "previous recap\n") writeAnalyzeFile(t, previousPath, "previous recap\n")
m.Inputs = append(m.Inputs, manifest.InputRecord{ setCurrentPreviousArtifactInput(t, m, previousPath)
Kind: "previous_artifact",
Path: previousPath,
})
sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{ sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{
@@ -894,10 +889,7 @@ func TestAnalyzeDoesNotCallObjectStoreForCanonicalPreviousSessionInput(t *testin
previousPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md") previousPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md")
writeAnalyzeFile(t, previousPath, "previous recap\n") writeAnalyzeFile(t, previousPath, "previous recap\n")
m.Inputs = append(m.Inputs, manifest.InputRecord{ setCurrentPreviousArtifactInput(t, m, previousPath)
Kind: "previous_artifact",
Path: previousPath,
})
sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{ sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{

View File

@@ -11,6 +11,7 @@ import (
"strings" "strings"
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/fileops" "gitea.maximumdirect.net/eric/narratio/internal/fileops"
"gitea.maximumdirect.net/eric/narratio/internal/manifest" "gitea.maximumdirect.net/eric/narratio/internal/manifest"
@@ -79,11 +80,15 @@ func hydratePreviousSessionArtifacts(
return nil, fmt.Errorf("install previous-session object %q: %w", record.RemoteKey, err) return nil, fmt.Errorf("install previous-session object %q: %w", record.RemoteKey, err)
} }
source := preparePreviousInputSource
if record.Kind == preparePreviousInputKindArtifact {
source = artifactpolicy.PreviousSessionSourceID(record.RequirementName)
}
result.Inputs = append(result.Inputs, manifest.InputRecord{ result.Inputs = append(result.Inputs, manifest.InputRecord{
Kind: record.Kind, Kind: record.Kind,
Path: record.LocalPath, Path: record.LocalPath,
Checksum: checksum, Checksum: checksum,
Source: preparePreviousInputSource, Source: source,
S3Bucket: record.S3Bucket, S3Bucket: record.S3Bucket,
S3Key: record.RemoteKey, S3Key: record.RemoteKey,
}) })

View File

@@ -70,8 +70,8 @@ func TestHydratePreviousSessionArtifactsDownloadsManifestAndRequiredArtifact(t *
if artifactInput == nil { if artifactInput == nil {
t.Fatalf("missing input kind %q", preparePreviousInputKindArtifact) t.Fatalf("missing input kind %q", preparePreviousInputKindArtifact)
} }
if artifactInput.Source != preparePreviousInputSource { if artifactInput.Source != "narratio.previous_session.artifact.session_recap" {
t.Fatalf("artifact input source = %q, want %q", artifactInput.Source, preparePreviousInputSource) t.Fatalf("artifact input source = %q, want canonical previous source", artifactInput.Source)
} }
if artifactInput.Path != recapPath { if artifactInput.Path != recapPath {
t.Fatalf("artifact input path = %q, want %q", artifactInput.Path, recapPath) t.Fatalf("artifact input path = %q, want %q", artifactInput.Path, recapPath)

View File

@@ -910,9 +910,6 @@ func TestPublishFailurePreservesPreviousCommittedSnapshotAndRetryIsDeterministic
if !sort.StringsAreSorted(destinations) { if !sort.StringsAreSorted(destinations) {
t.Fatalf("retried immutable mapping is not deterministic: %v", destinations) t.Fatalf("retried immutable mapping is not deterministic: %v", destinations)
} }
if _, exists := fake.Objects[m.S3SessionPrefix+"current/manifest.json"]; exists {
t.Fatal("new publication wrote legacy current manifest")
}
} }
func TestPublishRejectsConflictingImmutableObject(t *testing.T) { func TestPublishRejectsConflictingImmutableObject(t *testing.T) {