From e9c4873030e856755a4c9e73ed8de7356d3cd55e Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 2 Sep 2026 14:18:51 +0000 Subject: [PATCH] Complete compatibility purge audit --- docs/internal/artifacts.md | 9 +- docs/internal/manifest.md | 1 - docs/internal/stage-normalize.md | 2 +- docs/internal/stage-polish.md | 2 +- docs/internal/stage-render.md | 2 +- docs/roadmap/implementation.md | 2 +- docs/roadmap/refactoring.md | 2 +- .../adapters/storage/temp_download_test.go | 8 +- internal/app/artifact_evidence_test.go | 12 ++ internal/app/restore_test.go | 8 +- internal/app/restore_workflow_test.go | 1 + internal/artifacts/artifact_resolver.go | 131 +++++------------- internal/artifacts/artifact_resolver_test.go | 35 ++--- .../analyze_evidence_test_helpers_test.go | 12 ++ internal/stage/analyze_input_identity_test.go | 2 +- internal/stage/analyze_test.go | 16 +-- internal/stage/prepare_previous.go | 7 +- internal/stage/prepare_previous_test.go | 4 +- internal/stage/publish_test.go | 3 - 19 files changed, 99 insertions(+), 160 deletions(-) diff --git a/docs/internal/artifacts.md b/docs/internal/artifacts.md index a383dd7..cdc69ec 100644 --- a/docs/internal/artifacts.md +++ b/docs/internal/artifacts.md @@ -140,7 +140,8 @@ Extraction sources (`narratio.extraction.*`): 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 canonical source in the selected remote commit; and - never infer a path from a previous manifest, current configuration, basename, @@ -202,10 +203,9 @@ Core helpers: - `ValidateCurrentStateIdentity` - `RemoteCommitManifest` and `CurrentCommitPointer` -Typed missing-state errors: +Typed missing-state error: -- `CurrentRunPointerMissingError` (`ErrCurrentRunPointerMissing`) -- `CurrentManifestMissingError` (`ErrCurrentManifestMissing`) +- `CurrentCommitPointerMissingError` (`ErrCurrentCommitPointerMissing`) Identity validation supports caller-provided expectations: @@ -262,7 +262,6 @@ physical layout. `internal/artifacts/prepared_input.go` - Current state: `internal/artifacts/current_state.go`, `internal/artifacts/current_state_commit.go`, - `internal/artifacts/current_state_legacy.go` - Paths and keys: `internal/artifacts/paths.go`, `internal/artifacts/s3_keys.go` - Previous requirements: `internal/artifacts/previous_requirements.go` diff --git a/docs/internal/manifest.md b/docs/internal/manifest.md index 0f47d83..d99a4b2 100644 --- a/docs/internal/manifest.md +++ b/docs/internal/manifest.md @@ -315,7 +315,6 @@ where a durable running record can require operator interpretation. `internal/manifest/run_manifest.go` - Remote commit model and readers: `internal/artifacts/remote_commit.go`, `internal/artifacts/current_state_commit.go`, - `internal/artifacts/current_state_legacy.go` - Persistence and validation: `internal/manifest/store.go` - Package tests: `internal/manifest/*_test.go` - Assembled execution behavior: `internal/app/runner_test.go`, diff --git a/docs/internal/stage-normalize.md b/docs/internal/stage-normalize.md index 8a3fb7e..222c76b 100644 --- a/docs/internal/stage-normalize.md +++ b/docs/internal/stage-normalize.md @@ -15,7 +15,7 @@ Normalize polished transcript into final transcript using Seriatim. ## 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. - runs Seriatim normalize with configured timeout/binary. - validates normalized transcript and optional report. diff --git a/docs/internal/stage-polish.md b/docs/internal/stage-polish.md index b74acf3..b24fcc0 100644 --- a/docs/internal/stage-polish.md +++ b/docs/internal/stage-polish.md @@ -16,7 +16,7 @@ Run Audita polishing on base transcript and produce polished transcript. ## 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 invocation supplies paths and modules. - validates processed transcript structure (`segments` array required). diff --git a/docs/internal/stage-render.md b/docs/internal/stage-render.md index d5e737e..4ca48b8 100644 --- a/docs/internal/stage-render.md +++ b/docs/internal/stage-render.md @@ -21,7 +21,7 @@ records that may consume rendered transcripts. ## Key Behavior - 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. - records input provenance, output paths, adapter metadata, logs, and generated config refs. - when `pipeline.render.enabled=false`, completes successfully with no outputs diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index cf94b6b..e886b0a 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -373,7 +373,7 @@ consumers require current manifest evidence. ## Stage 7 — Complete The Purge And Validate The Repository -**Status: Pending** +**Status: Completed** ### Goal diff --git a/docs/roadmap/refactoring.md b/docs/roadmap/refactoring.md index 000b8cc..bfc4e46 100644 --- a/docs/roadmap/refactoring.md +++ b/docs/roadmap/refactoring.md @@ -150,7 +150,7 @@ keys remain protected without treating ordinary D&D data as private. ### Workstream 3 — Canonical contracts and compatibility purge -**Status:** Pending +**Status:** Complete **Audit sources:** H43 and C01. diff --git a/internal/adapters/storage/temp_download_test.go b/internal/adapters/storage/temp_download_test.go index 6f6ea48..8322395 100644 --- a/internal/adapters/storage/temp_download_test.go +++ b/internal/adapters/storage/temp_download_test.go @@ -12,9 +12,9 @@ import ( func TestDownloadObjectToTempSuccess(t *testing.T) { 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 { t.Fatalf("DownloadObjectToTemp() error = %v", err) } @@ -38,7 +38,7 @@ func TestDownloadObjectToTempFailedDownloadRemovesTempFile(t *testing.T) { 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) { t.Fatalf("DownloadObjectToTemp() error = %v, want %v", err, sentinel) } @@ -58,7 +58,7 @@ func TestDownloadObjectToTempCallerContextWrappingPreservesCause(t *testing.T) { sentinel := errors.New("object missing") 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 { t.Fatal("DownloadObjectToTemp() error = nil, want error") } diff --git a/internal/app/artifact_evidence_test.go b/internal/app/artifact_evidence_test.go index e052310..5d84b64 100644 --- a/internal/app/artifact_evidence_test.go +++ b/internal/app/artifact_evidence_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) @@ -29,3 +30,14 @@ func seedCurrentBuiltInEvidence(t *testing.T, manifestPath, sessionID, campaign, 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, + }) +} diff --git a/internal/app/restore_test.go b/internal/app/restore_test.go index 2f53f7c..6bac65e 100644 --- a/internal/app/restore_test.go +++ b/internal/app/restore_test.go @@ -64,7 +64,7 @@ func TestExecuteRestoreRecognizedAndReturnsNYI(t *testing.T) { { Kind: RestoreActionDownload, 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", }, }, @@ -165,7 +165,7 @@ func TestExecuteRestoreDiscoveryErrorSurfaced(t *testing.T) { return &storage.FakeBackend{}, nil } 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() @@ -177,7 +177,7 @@ func TestExecuteRestoreDiscoveryErrorSurfaced(t *testing.T) { if code == 0 { 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()) } } @@ -250,7 +250,7 @@ func TestExecuteRestoreLoadsSecretsBeforeObjectStoreInit(t *testing.T) { { Kind: RestoreActionDownload, 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", }, }, diff --git a/internal/app/restore_workflow_test.go b/internal/app/restore_workflow_test.go index 8f335d4..15a4efe 100644 --- a/internal/app/restore_workflow_test.go +++ b/internal/app/restore_workflow_test.go @@ -211,6 +211,7 @@ previous_session_id: 2026-04-26 t.Fatalf("load restored manifest: %v", err) } 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 { t.Fatalf("save manifest evidence: %v", err) } diff --git a/internal/artifacts/artifact_resolver.go b/internal/artifacts/artifact_resolver.go index a8e01fe..ae22611 100644 --- a/internal/artifacts/artifact_resolver.go +++ b/internal/artifacts/artifact_resolver.go @@ -29,7 +29,6 @@ const ( ArtifactBoundsSession = "narratio.bounds.session" ArtifactProvenancePreviousCacheManifestInput = "manifest.inputs.previous_cache" - ArtifactProvenancePreviousCacheFilesystem = "current_session.previous_cache" ) const ( @@ -264,54 +263,49 @@ func ResolvePreviousSessionArtifactWithCatalog( return ResolvedSessionArtifact{}, fmt.Errorf("previous-session artifact source %q requires runtime artifact catalog", source) } - configuredSourceID := ConfiguredArtifactSourceID(artifactName) - entry, ok := catalog.Lookup(configuredSourceID) - if !ok { + if _, ok := catalog.Lookup(ConfiguredArtifactSourceID(artifactName)); !ok { return ResolvedSessionArtifact{}, fmt.Errorf("unsupported previous-session artifact source %q", source) } - candidates := previousSessionCacheCandidatePaths(paths, entry.CanonicalRelPath) - if len(candidates) == 0 { + if m == nil { return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: source} } - - manifestInputPaths := manifestInputPathSet(paths, m) - fallback := "" - for _, candidate := range candidates { - exists, isDir, statErr := pathExists(candidate) - if statErr != nil { - return ResolvedSessionArtifact{}, fmt.Errorf("stat %q: %w", candidate, statErr) - } - if !exists || isDir { + var selected manifest.InputRecord + count := 0 + for _, input := range m.Inputs { + if input.Kind != "previous_artifact" || input.Source != source { continue } - if err := validateResolvedContent(candidate, contentText); err != nil { - return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: %w", source, err) + candidate := filepath.Clean(ResolveSessionLocalPathForRead(paths, input.Path)) + if strings.TrimSpace(candidate) == "" { + continue } - if _, ok := manifestInputPaths[candidate]; ok { - return ResolvedSessionArtifact{ - ID: source, - Path: candidate, - ProducerStage: "prepare", - OutputKind: "previous_session_cache", - Provenance: ArtifactProvenancePreviousCacheManifestInput, - }, nil - } - if fallback == "" { - fallback = candidate + if _, err := pathsafe.SlashRelativeFromRoot(paths.Root, candidate); err != nil { + continue } + selected, count = input, count+1 } - - if fallback != "" { - return ResolvedSessionArtifact{ - ID: source, - Path: fallback, - ProducerStage: "prepare", - OutputKind: "previous_session_cache", - Provenance: ArtifactProvenancePreviousCacheFilesystem, - }, nil + if count != 1 { + return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: source} } - - return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: source} + path := filepath.Clean(ResolveSessionLocalPathForRead(paths, selected.Path)) + 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) { @@ -384,65 +378,6 @@ func requireSafeSessionRegularFile(root, path string) error { 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 { switch kind { case contentTranscriptJSON: diff --git a/internal/artifacts/artifact_resolver_test.go b/internal/artifacts/artifact_resolver_test.go index f2b64d7..645fc1c 100644 --- a/internal/artifacts/artifact_resolver_test.go +++ b/internal/artifacts/artifact_resolver_test.go @@ -4,11 +4,11 @@ import ( "errors" "os" "path/filepath" - "reflect" "strings" "testing" "time" + "gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) @@ -474,9 +474,13 @@ func TestResolvePreviousSessionArtifactWithCatalogPrefersManifestInputRecord(t * } m := manifest.New("session", time.Now().UTC()) - m.Inputs = []manifest.InputRecord{ - {Kind: "previous_artifact", Path: manifestBackedPath}, + checksum, err := SHA256File(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( paths, @@ -495,7 +499,7 @@ func TestResolvePreviousSessionArtifactWithCatalogPrefersManifestInputRecord(t * } } -func TestResolvePreviousSessionArtifactWithCatalogFallsBackToPreparedCachePath(t *testing.T) { +func TestResolvePreviousSessionArtifactWithCatalogRejectsIncidentalPreparedCachePath(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") @@ -517,31 +521,14 @@ func TestResolvePreviousSessionArtifactWithCatalogFallsBackToPreparedCachePath(t t.Fatalf("RegisterConfiguredArtifacts() error = %v", err) } - resolved, err := ResolvePreviousSessionArtifactWithCatalog( + _, err := ResolvePreviousSessionArtifactWithCatalog( paths, nil, "narratio.previous_session.artifact.session_recap", catalog, ) - if err != nil { - t.Fatalf("ResolvePreviousSessionArtifactWithCatalog() error = %v", 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) + if !errors.Is(err, ErrSessionArtifactNotFound) { + t.Fatalf("ResolvePreviousSessionArtifactWithCatalog() error = %v, want unavailable incidental cache", err) } } diff --git a/internal/stage/analyze_evidence_test_helpers_test.go b/internal/stage/analyze_evidence_test_helpers_test.go index ba424e1..f0d2287 100644 --- a/internal/stage/analyze_evidence_test_helpers_test.go +++ b/internal/stage/analyze_evidence_test_helpers_test.go @@ -7,6 +7,7 @@ import ( "time" "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/manifest" ) @@ -55,3 +56,14 @@ func setCurrentBuiltInEvidence(t *testing.T, m *manifest.Manifest, stage, kind, 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, + }) +} diff --git a/internal/stage/analyze_input_identity_test.go b/internal/stage/analyze_input_identity_test.go index 3f8142e..aaa3f57 100644 --- a/internal/stage/analyze_input_identity_test.go +++ b/internal/stage/analyze_input_identity_test.go @@ -61,7 +61,7 @@ func TestResolveAnalyzeInputIdentitiesSupportsEverySourceKind(t *testing.T) { setup: func(t *testing.T, env *Env, m *manifest.Manifest) string { path := mustPreviousArtifactPath(t, sessionPathsForEnv(env, m.SessionID), "artifacts/session_recap.md") writeAnalyzeFile(t, path, "previous recap\n") - m.Inputs = append(m.Inputs, manifest.InputRecord{Kind: "previous_artifact", Path: path}) + setCurrentPreviousArtifactInput(t, m, path) return path }, wantOutputKind: "previous_session_cache", diff --git a/internal/stage/analyze_test.go b/internal/stage/analyze_test.go index 0045e68..fe972b7 100644 --- a/internal/stage/analyze_test.go +++ b/internal/stage/analyze_test.go @@ -392,6 +392,7 @@ func TestAnalyzeIncludesCanonicalPreviousRecapWhenPreparedCacheExists(t *testing previousRecapPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md") writeAnalyzeFile(t, previousRecapPath, "previous recap\n") + setCurrentPreviousArtifactInput(t, m, previousRecapPath) env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{ Enabled: true, @@ -804,10 +805,7 @@ func TestAnalyzeResolvesCanonicalPreviousSessionArtifactFromManifestInput(t *tes previousPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md") writeAnalyzeFile(t, previousPath, "previous recap\n") - m.Inputs = append(m.Inputs, manifest.InputRecord{ - Kind: "previous_artifact", - Path: previousPath, - }) + setCurrentPreviousArtifactInput(t, m, previousPath) sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{ @@ -860,10 +858,7 @@ func TestAnalyzeRenderDebugWithCanonicalPreviousSessionInput(t *testing.T) { previousPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md") writeAnalyzeFile(t, previousPath, "previous recap\n") - m.Inputs = append(m.Inputs, manifest.InputRecord{ - Kind: "previous_artifact", - Path: previousPath, - }) + setCurrentPreviousArtifactInput(t, m, previousPath) sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{ @@ -894,10 +889,7 @@ func TestAnalyzeDoesNotCallObjectStoreForCanonicalPreviousSessionInput(t *testin previousPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md") writeAnalyzeFile(t, previousPath, "previous recap\n") - m.Inputs = append(m.Inputs, manifest.InputRecord{ - Kind: "previous_artifact", - Path: previousPath, - }) + setCurrentPreviousArtifactInput(t, m, previousPath) sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{ diff --git a/internal/stage/prepare_previous.go b/internal/stage/prepare_previous.go index 09edeb3..55b7975 100644 --- a/internal/stage/prepare_previous.go +++ b/internal/stage/prepare_previous.go @@ -11,6 +11,7 @@ import ( "strings" "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/fileops" "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) } + source := preparePreviousInputSource + if record.Kind == preparePreviousInputKindArtifact { + source = artifactpolicy.PreviousSessionSourceID(record.RequirementName) + } result.Inputs = append(result.Inputs, manifest.InputRecord{ Kind: record.Kind, Path: record.LocalPath, Checksum: checksum, - Source: preparePreviousInputSource, + Source: source, S3Bucket: record.S3Bucket, S3Key: record.RemoteKey, }) diff --git a/internal/stage/prepare_previous_test.go b/internal/stage/prepare_previous_test.go index 0e83cac..90eb657 100644 --- a/internal/stage/prepare_previous_test.go +++ b/internal/stage/prepare_previous_test.go @@ -70,8 +70,8 @@ func TestHydratePreviousSessionArtifactsDownloadsManifestAndRequiredArtifact(t * if artifactInput == nil { t.Fatalf("missing input kind %q", preparePreviousInputKindArtifact) } - if artifactInput.Source != preparePreviousInputSource { - t.Fatalf("artifact input source = %q, want %q", artifactInput.Source, preparePreviousInputSource) + if artifactInput.Source != "narratio.previous_session.artifact.session_recap" { + t.Fatalf("artifact input source = %q, want canonical previous source", artifactInput.Source) } if artifactInput.Path != recapPath { t.Fatalf("artifact input path = %q, want %q", artifactInput.Path, recapPath) diff --git a/internal/stage/publish_test.go b/internal/stage/publish_test.go index ac74af8..e7879b3 100644 --- a/internal/stage/publish_test.go +++ b/internal/stage/publish_test.go @@ -910,9 +910,6 @@ func TestPublishFailurePreservesPreviousCommittedSnapshotAndRetryIsDeterministic if !sort.StringsAreSorted(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) {