Simplified the output of narratio artifacts list --remote and narratio status --session-id

This commit is contained in:
2026-05-21 21:20:41 -05:00
parent ca1ded1821
commit 3022f20beb
4 changed files with 32 additions and 53 deletions

View File

@@ -132,7 +132,7 @@ Valid stage names:
- `--session <path>` - `--session <path>`
- `--session-id <value>` - `--session-id <value>`
- `--previous-session-id <value>` - `--previous-session-id <value>`
- `--remote`: check remote object availability. Catalog sections use canonical source paths; the `Promoted` section checks configured archive destinations. - `--remote`: check remote availability for configured archive promotion destinations.
### `locks` ### `locks`
@@ -288,8 +288,7 @@ Syntax:
narratio artifacts list [--config <pipeline.yml>] [--campaign <campaign.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--remote] narratio artifacts list [--config <pipeline.yml>] [--campaign <campaign.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--remote]
``` ```
`--remote` checks promoted top-level object availability through the storage adapter. `--remote` checks promoted top-level object availability through the storage adapter. Remote markers appear only in the `Promoted` section, which reports each configured archive promotion destination and includes `dest=<path>` when that destination differs from the source's canonical path.
Catalog sections report canonical source-path availability. The `Promoted` section reports each configured archive promotion destination and includes `dest=<path>` when that destination differs from the source's canonical path.
### `locks` ### `locks`

View File

@@ -253,7 +253,7 @@ Dry-run does not write restore report files.
- `status` with no config/session flags still requires explicit `--manifest`. - `status` with no config/session flags still requires explicit `--manifest`.
- `status --session-id <id>` uses normal config/session loading, including remote session fallback. - `status --session-id <id>` uses normal config/session loading, including remote session fallback.
- `status --session-id <id>` includes the same remote output availability view as `artifacts list --remote` when storage is configured: catalog sections use canonical source paths, and promoted outputs use configured archive destinations. - `status --session-id <id>` includes the same promoted remote output availability view as `artifacts list --remote` when storage is configured.
- local and S3 audio input modes are mutually exclusive. - local and S3 audio input modes are mutually exclusive.
- archive publish requires upstream stages through `analyze` to be `succeeded`. - archive publish requires upstream stages through `analyze` to be `succeeded`.
- required promotion rules can fail when selected analyze artifacts did not generate a required file path. - required promotion rules can fail when selected analyze artifacts did not generate a required file path.

View File

@@ -218,14 +218,12 @@ func Status(ctx context.Context, args []string, out io.Writer) error {
All: staticArchiveLocks(cfg), All: staticArchiveLocks(cfg),
} }
} }
remoteState := map[string]string{}
promotedRemoteState := map[string]string{} promotedRemoteState := map[string]string{}
if store != nil { if store != nil {
remoteState = remoteArtifactAvailability(ctx, cfg, store, catalog)
promotedRemoteState = remotePromotionAvailability(ctx, cfg, store, catalog) promotedRemoteState = remotePromotionAvailability(ctx, cfg, store, catalog)
} }
fmt.Fprintln(out, "Remote outputs:") fmt.Fprintln(out, "Remote outputs:")
writeArtifactList(out, cfg, catalog, catalogLocks, remoteState, promotedRemoteState) writeArtifactList(out, cfg, catalog, catalogLocks, promotedRemoteState)
} }
if err != nil { if err != nil {
fmt.Fprintf(out, "Archive locks: error: %v\n", err) fmt.Fprintf(out, "Archive locks: error: %v\n", err)
@@ -390,13 +388,11 @@ func ArtifactsList(ctx context.Context, args []string, out io.Writer) error {
if err != nil { if err != nil {
return fmt.Errorf("artifacts list: %w", err) return fmt.Errorf("artifacts list: %w", err)
} }
remoteState := map[string]string{}
promotedRemoteState := map[string]string{} promotedRemoteState := map[string]string{}
if remote && store != nil { if remote && store != nil {
remoteState = remoteArtifactAvailability(ctx, cfg, store, catalog)
promotedRemoteState = remotePromotionAvailability(ctx, cfg, store, catalog) promotedRemoteState = remotePromotionAvailability(ctx, cfg, store, catalog)
} }
writeArtifactList(out, cfg, catalog, locks, remoteState, promotedRemoteState) writeArtifactList(out, cfg, catalog, locks, promotedRemoteState)
return nil return nil
} }
@@ -797,7 +793,7 @@ func buildHelperArtifactCatalog(cfg *config.Config) (*artifacts.ArtifactCatalog,
return catalog, nil return catalog, nil
} }
func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.ArtifactCatalog, locks *effectiveLocks, remoteState map[string]string, promotedRemoteState map[string]string) { func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.ArtifactCatalog, locks *effectiveLocks, promotedRemoteState map[string]string) {
lockSet := lockSourceSet(locks.All) lockSet := lockSourceSet(locks.All)
fmt.Fprintln(out, "Built-in:") fmt.Fprintln(out, "Built-in:")
for _, id := range []string{ for _, id := range []string{
@@ -807,11 +803,11 @@ func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.Art
artifacts.ArtifactTranscriptTrimmed, artifacts.ArtifactTranscriptTrimmed,
artifacts.ArtifactBoundsSession, artifacts.ArtifactBoundsSession,
} { } {
writeArtifactLine(out, id, lockSet, remoteState) writeArtifactLine(out, id, lockSet)
} }
fmt.Fprintln(out, "Configured:") fmt.Fprintln(out, "Configured:")
for _, entry := range catalog.ListConfigured() { for _, entry := range catalog.ListConfigured() {
writeArtifactLine(out, entry.SourceID, lockSet, remoteState) writeArtifactLine(out, entry.SourceID, lockSet)
} }
fmt.Fprintln(out, "Previous-session:") fmt.Fprintln(out, "Previous-session:")
for _, req := range artifacts.CollectPreviousArtifactRequirements(configuredScriptoriumArtifacts(cfg)) { for _, req := range artifacts.CollectPreviousArtifactRequirements(configuredScriptoriumArtifacts(cfg)) {
@@ -823,14 +819,11 @@ func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.Art
} }
} }
func writeArtifactLine(out io.Writer, source string, lockSet map[string]config.ArchiveLockRule, remoteState map[string]string) { func writeArtifactLine(out io.Writer, source string, lockSet map[string]config.ArchiveLockRule) {
parts := []string{source} parts := []string{source}
if _, ok := lockSet[source]; ok { if _, ok := lockSet[source]; ok {
parts = append(parts, "locked") parts = append(parts, "locked")
} }
if state := remoteState[source]; state != "" {
parts = append(parts, state)
}
fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) fmt.Fprintf(out, "- %s\n", strings.Join(parts, " "))
} }
@@ -855,26 +848,6 @@ func writePromotedArtifactLine(out io.Writer, rule config.ArchivePromotionRule,
fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) fmt.Fprintf(out, "- %s\n", strings.Join(parts, " "))
} }
func remoteArtifactAvailability(ctx context.Context, cfg *config.Config, store storage.ObjectStore, catalog *artifacts.ArtifactCatalog) map[string]string {
out := map[string]string{}
sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID)
for _, source := range allCatalogSources(catalog) {
entry, ok := catalog.Lookup(source)
if !ok || strings.TrimSpace(entry.CanonicalRelPath) == "" {
continue
}
key := artifacts.S3PromotedArtifactKey(sessionPrefix, entry.CanonicalRelPath)
if exists, err := store.Exists(ctx, key); err == nil && exists {
out[source] = "remote=promoted"
} else if err != nil {
out[source] = "remote=error"
} else {
out[source] = "remote=missing"
}
}
return out
}
func remotePromotionAvailability(ctx context.Context, cfg *config.Config, store storage.ObjectStore, catalog *artifacts.ArtifactCatalog) map[string]string { func remotePromotionAvailability(ctx context.Context, cfg *config.Config, store storage.ObjectStore, catalog *artifacts.ArtifactCatalog) map[string]string {
out := map[string]string{} out := map[string]string{}
sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID) sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID)
@@ -938,20 +911,6 @@ func promotionRemoteStateKey(source, dest string) string {
return strings.TrimSpace(source) + "\x00" + strings.TrimSpace(dest) return strings.TrimSpace(source) + "\x00" + strings.TrimSpace(dest)
} }
func allCatalogSources(catalog *artifacts.ArtifactCatalog) []string {
out := []string{
artifacts.ArtifactTranscriptMerged,
artifacts.ArtifactTranscriptPolished,
artifacts.ArtifactTranscriptFull,
artifacts.ArtifactTranscriptTrimmed,
artifacts.ArtifactBoundsSession,
}
for _, entry := range catalog.ListConfigured() {
out = append(out, entry.SourceID)
}
return out
}
func writeLocks(out io.Writer, cfg *config.Config, locks *effectiveLocks) { func writeLocks(out io.Writer, cfg *config.Config, locks *effectiveLocks) {
if locks == nil || len(locks.All) == 0 { if locks == nil || len(locks.All) == 0 {
fmt.Fprintln(out, "Archive locks: none") fmt.Fprintln(out, "Archive locks: none")

View File

@@ -311,6 +311,12 @@ func TestExecuteTopLevelLockAndUnlockAreRemoved(t *testing.T) {
func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) { func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) {
workspaceRoot := t.TempDir() workspaceRoot := t.TempDir()
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
addArchivePromotionsToPipeline(t, pipelinePath, `
promote_artifacts:
- source: narratio.transcript.trimmed
dest: transcripts/trimmed.json
required: true
`)
fake := &storage.FakeBackend{} fake := &storage.FakeBackend{}
trimmedKey := artifacts.S3PromotedArtifactKey( trimmedKey := artifacts.S3PromotedArtifactKey(
artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"),
@@ -369,9 +375,15 @@ func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
} }
out := stdout.String() out := stdout.String()
for _, want := range []string{ for _, unwanted := range []string{
"narratio.transcript.full remote=missing", "narratio.transcript.full remote=missing",
"narratio.bounds.session remote=missing", "narratio.bounds.session remote=missing",
} {
if strings.Contains(out, unwanted) {
t.Fatalf("stdout = %q, did not want catalog remote marker %q", out, unwanted)
}
}
for _, want := range []string{
"narratio.transcript.full dest=transcripts/full.json remote=promoted", "narratio.transcript.full dest=transcripts/full.json remote=promoted",
"narratio.bounds.session dest=transcripts/bounds.json remote=promoted", "narratio.bounds.session dest=transcripts/bounds.json remote=promoted",
} { } {
@@ -426,19 +438,28 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) {
"Configured:", "Configured:",
"Previous-session:", "Previous-session:",
"Promoted:", "Promoted:",
"narratio.transcript.trimmed locked",
"narratio.transcript.trimmed locked remote=promoted", "narratio.transcript.trimmed locked remote=promoted",
"narratio.transcript.merged remote=missing",
"narratio.transcript.full dest=transcripts/full.json remote=promoted", "narratio.transcript.full dest=transcripts/full.json remote=promoted",
} { } {
if !strings.Contains(out, want) { if !strings.Contains(out, want) {
t.Fatalf("stdout = %q, want %q", out, want) t.Fatalf("stdout = %q, want %q", out, want)
} }
} }
if strings.Contains(out, "narratio.transcript.merged remote=missing") {
t.Fatalf("stdout = %q, did not want catalog remote marker", out)
}
} }
func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testing.T) { func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testing.T) {
workspaceRoot := t.TempDir() workspaceRoot := t.TempDir()
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
addArchivePromotionsToPipeline(t, pipelinePath, `
promote_artifacts:
- source: narratio.transcript.trimmed
dest: transcripts/trimmed.json
required: true
`)
fake := &storage.FakeBackend{ExistsErr: fmt.Errorf("exists failed")} fake := &storage.FakeBackend{ExistsErr: fmt.Errorf("exists failed")}
var storeInitCalls int var storeInitCalls int
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})