From b750c42ad67fde63e2f9e7f9b8494c8d56ea90db Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 25 Sep 2026 19:45:19 +0000 Subject: [PATCH] Remove legacy manifest provenance decoding --- docs/internal/manifest.md | 10 +++++----- docs/operations.md | 24 ++++++++++++++++++++++++ docs/roadmap/audita.md | 7 +++++-- docs/roadmap/implementation.md | 13 ++++++++----- docs/roadmap/remediation.md | 20 ++++++++++++++++++-- internal/manifest/manifest.go | 13 ------------- internal/manifest/store_test.go | 25 +++++++++++++------------ 7 files changed, 73 insertions(+), 39 deletions(-) diff --git a/docs/internal/manifest.md b/docs/internal/manifest.md index 21497c1..7292133 100644 --- a/docs/internal/manifest.md +++ b/docs/internal/manifest.md @@ -23,11 +23,11 @@ portable opaque segments. Unsafe legacy identities are rejected with migration guidance rather than being normalized into a different workspace or remote namespace. -Decoding is strict at typed execution-state fields. Unknown informational fields -inside `effective_config` are ignored so older selection labels can be read; -unknown fields elsewhere are rejected. The schema version remains -`narratio.session-manifest.v1` and there is no compatibility reader that -silently drops retired execution state. Cleanup progress uses +Decoding rejects unknown typed fields, including informational fields inside +`effective_config` and its nested selection. The schema version remains +`narratio.session-manifest.v1`; there is no compatibility reader that silently +drops retired fields. See [overlay migration](../operations.md#migrating-manifest-overlay-provenance) +for deliberate migration of retired selection provenance. Cleanup progress uses only the cleanup entry in `stages`. Every persisted session manifest has a non-nil `stages` map. Each entry is diff --git a/docs/operations.md b/docs/operations.md index 635cd88..c5f6d80 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -410,6 +410,30 @@ execute. Deleting Narratio's local reference snapshots does not invalidate a complete durable extraction; missing snapshots are reconstructed only when a later extraction actually runs. +## Migrating Manifest Overlay Provenance + +The pipeline-selection rename is a clean cutover. Manifests containing +`effective_config.selected_profile` are rejected by strict decoding; Narratio +does not silently discard or translate that retired field. + +Before using an affected local session with the new version: + +1. Stop Narratio activity for the session and back up its manifest. +2. Rename only `effective_config.selected_profile` to `selected_overlay`, keeping + its `name` and `source`, or remove the retired informational member. If a + current `selected_overlay` member already exists, retain it and remove the + retired member rather than creating duplicate keys. +3. Preserve the schema version, digest, stage records, checkpoint evidence, + output references, and other session state. Retry the intended command. + +This metadata is informational, not resume authority. Do not delete session +progress or force all stages merely to perform this migration. Other unknown +fields still require investigation rather than blanket removal. + +Do not edit immutable remote committed snapshots. Restoring an older snapshot +with retired provenance requires a compatible previous Narratio version, followed +by migration of the restored local manifest before using the new version. + ## Migrating Audita and Provider Credentials This is a breaking clean cutover to Audita v2. Narratio does not translate an diff --git a/docs/roadmap/audita.md b/docs/roadmap/audita.md index 45ea4c5..ac64e39 100644 --- a/docs/roadmap/audita.md +++ b/docs/roadmap/audita.md @@ -93,8 +93,11 @@ newly written selection provenance use overlay terminology as well. Maintain existing value-based digest and stage-reuse semantics; renaming informational selection provenance is not a reason to invalidate completed -work or rewrite historical manifests. Update maintained examples and operator -migration guidance for the directory and flag changes. +work or automatically rewrite historical manifests. Strict manifest decoding +rejects retired provenance fields; operators must deliberately migrate that +informational member on a backed-up manifest, preserving execution/checkpoint +state. Update maintained examples and operator migration guidance for the +directory, flag, and provenance changes. This rename does not apply to downstream model profiles, Scriptorium `profile_id`, Audita's proposal/validation profiles, or Notarius's diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 560c850..4ef5b5e 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -46,9 +46,11 @@ as base selection; do not add a persistent default selector or new precedence. Rename newly written informational provenance to overlay terminology, including `selected_overlay` in manifest configuration provenance. Do not add a dual-field -reader, rewrite historical manifests, or introduce a manifest-version rejection -solely for this informational rename. Existing generic decoding behavior applies -to old optional provenance; it does not become resume authority. Preserve +reader, automatically rewrite historical manifests, or introduce a manifest-version +rejection solely for this informational rename. Existing strict typed decoding +rejects retired provenance fields. Document backed-up operator migration of +that informational member without changing execution/checkpoint state; it does +not become resume authority. Preserve semantic fingerprints and effective-value digest rules when values are unchanged. Only Narratio pipeline configuration selection is renamed. Keep model/profile @@ -199,8 +201,9 @@ config commands in `internal/app/`; configuration provenance in directory is not a discovery fallback. Do not freeze incidental error wording. 5. Verify unchanged effective values retain digest and stage-reuse behavior; only diagnostic source labels/paths and selected provenance terminology change. - Test loading representative existing manifests without inventing a new - invalidation or rejection gate for old optional selection metadata. + Test current provenance round trips and strict rejection of unknown/retired + provenance fields. Document deliberate operator migration; do not add a + compatibility decoder, semantic invalidation rule, or schema-version gate. 6. Run focused and full tests before proceeding so failures from the dependency and terminology change are separated from adapter migrations. No Audita or Notarius credential/CLI behavior changes in this stage. diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 1bf260b..80af85a 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -1,6 +1,6 @@ # Audita v2 and ConfigKit Migration Review Remediation -Status: Pending. +Status: Complete. Reviewed `83eaa96` through `fae18bf` against the approved clean-cutover decisions, [feature roadmap](audita.md), and [implementation plan](implementation.md). @@ -46,7 +46,14 @@ Required remediation: ## 2. Remove the unapproved manifest compatibility decoder -Priority: P2. Status: Pending. +Priority: P2. Status: Complete. + +Removed the permissive custom decoder; ordinary strict typed decoding now +applies to provenance as well. Replaced historical-acceptance coverage with +retired/unknown-field rejection tests, all of which failed before removal. +Current provenance round-trip coverage remains intact. Corrected the roadmap, +plan, internal contract, and operator guidance for narrow backed-up migration +without discarding execution/checkpoint state or changing resume authority. Location: `internal/manifest/manifest.go:79-89`, `internal/manifest/store_test.go:78`, and `docs/internal/manifest.md`'s decoding @@ -92,6 +99,8 @@ reader. Do not treat that exception as already granted. ## Review verification +The following records the original review, before remediation. + - Existing `go test ./...` passed. - Race tests passed for subprocess, Audita, Notarius, config, stage, app, and manifest packages after removing the temporary reproduction. @@ -105,6 +114,13 @@ shared named credential forwarding/redaction, a v2-only Audita subprocess adapter, ordinary reports as diagnostics, and versioned ordered-module polish evidence. No live model/provider runs were performed during this review. +## Migration remediation verification + +Completed on 2026-09-25. Both new regressions failed before their respective +fixes. After remediation, `go test ./...`, `go test -race ./...`, +`go vet ./...`, `go build ./...`, documentation checks, maintained configuration +example checks, and `git diff --check` passed. No live provider runs were needed. + ## Historical completed transcription review Status: Complete. diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index fecc283..fa76fb5 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -1,7 +1,6 @@ package manifest import ( - "encoding/json" "strings" "time" @@ -76,18 +75,6 @@ type EffectiveConfigProvenance struct { EffectiveConfigDigest string `json:"effective_config_digest,omitempty"` } -// UnmarshalJSON accepts unknown informational provenance from older manifests. -// The enclosing manifest and its execution state remain strictly decoded. -func (p *EffectiveConfigProvenance) UnmarshalJSON(data []byte) error { - type provenance EffectiveConfigProvenance - var decoded provenance - if err := json.Unmarshal(data, &decoded); err != nil { - return err - } - *p = EffectiveConfigProvenance(decoded) - return nil -} - // SelectedOverlayProvenance identifies an explicitly or default-selected // pipeline overlay without recording overlay content. type SelectedOverlayProvenance struct { diff --git a/internal/manifest/store_test.go b/internal/manifest/store_test.go index a6705d3..1474d83 100644 --- a/internal/manifest/store_test.go +++ b/internal/manifest/store_test.go @@ -75,18 +75,19 @@ func TestLocalStoreCreateSaveLoadRoundTrip(t *testing.T) { } } -func TestLocalStoreLoadsManifestWithHistoricalSelectionProvenance(t *testing.T) { - path := filepath.Join(t.TempDir(), "manifest.json") - data := `{"schema_version":"narratio.session-manifest.v1","session_id":"2026-05-03","created_at":"2026-05-03T10:00:00Z","updated_at":"2026-05-03T10:00:00Z","effective_config":{"selected_profile":{"name":"testing","source":"explicit"},"effective_config_digest":"001122"},"stages":{}}` - if err := os.WriteFile(path, []byte(data), 0o600); err != nil { - t.Fatal(err) - } - loaded, err := (&LocalStore{}).Load(context.Background(), path) - if err != nil { - t.Fatal(err) - } - if loaded.EffectiveConfig == nil || loaded.EffectiveConfig.EffectiveConfigDigest != "001122" || loaded.EffectiveConfig.SelectedOverlay != nil { - t.Fatalf("historical informational provenance = %#v", loaded.EffectiveConfig) +func TestLocalStoreRejectsUnknownProvenanceFields(t *testing.T) { + for _, tc := range []struct{ name, provenance, field string }{ + {"retired selection", `{"selected_profile":{"name":"testing","source":"explicit"}}`, "selected_profile"}, + {"unknown provenance", `{"unexpected":true}`, "unexpected"}, + {"unknown selection field", `{"selected_overlay":{"name":"testing","source":"explicit","unexpected":true}}`, "unexpected"}, + } { + t.Run(tc.name, func(t *testing.T) { + data := `{"schema_version":"narratio.session-manifest.v1","session_id":"2026-05-03","created_at":"2026-05-03T10:00:00Z","updated_at":"2026-05-03T10:00:00Z","effective_config":` + tc.provenance + `,"stages":{}}` + _, err := (&LocalStore{}).LoadReader(context.Background(), strings.NewReader(data)) + if err == nil || !strings.Contains(err.Error(), tc.field) { + t.Fatalf("expected rejection identifying %q, got %v", tc.field, err) + } + }) } }