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-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`
@@ -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]
```
`--remote` checks promoted top-level object availability through the storage adapter.
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.
`--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.
### `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 --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.
- 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.

View File

@@ -218,14 +218,12 @@ func Status(ctx context.Context, args []string, out io.Writer) error {
All: staticArchiveLocks(cfg),
}
}
remoteState := map[string]string{}
promotedRemoteState := map[string]string{}
if store != nil {
remoteState = remoteArtifactAvailability(ctx, cfg, store, catalog)
promotedRemoteState = remotePromotionAvailability(ctx, cfg, store, catalog)
}
fmt.Fprintln(out, "Remote outputs:")
writeArtifactList(out, cfg, catalog, catalogLocks, remoteState, promotedRemoteState)
writeArtifactList(out, cfg, catalog, catalogLocks, promotedRemoteState)
}
if err != nil {
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 {
return fmt.Errorf("artifacts list: %w", err)
}
remoteState := map[string]string{}
promotedRemoteState := map[string]string{}
if remote && store != nil {
remoteState = remoteArtifactAvailability(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
}
@@ -797,7 +793,7 @@ func buildHelperArtifactCatalog(cfg *config.Config) (*artifacts.ArtifactCatalog,
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)
fmt.Fprintln(out, "Built-in:")
for _, id := range []string{
@@ -807,11 +803,11 @@ func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.Art
artifacts.ArtifactTranscriptTrimmed,
artifacts.ArtifactBoundsSession,
} {
writeArtifactLine(out, id, lockSet, remoteState)
writeArtifactLine(out, id, lockSet)
}
fmt.Fprintln(out, "Configured:")
for _, entry := range catalog.ListConfigured() {
writeArtifactLine(out, entry.SourceID, lockSet, remoteState)
writeArtifactLine(out, entry.SourceID, lockSet)
}
fmt.Fprintln(out, "Previous-session:")
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}
if _, ok := lockSet[source]; ok {
parts = append(parts, "locked")
}
if state := remoteState[source]; state != "" {
parts = append(parts, state)
}
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, " "))
}
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 {
out := map[string]string{}
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)
}
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) {
if locks == nil || len(locks.All) == 0 {
fmt.Fprintln(out, "Archive locks: none")

View File

@@ -311,6 +311,12 @@ func TestExecuteTopLevelLockAndUnlockAreRemoved(t *testing.T) {
func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
addArchivePromotionsToPipeline(t, pipelinePath, `
promote_artifacts:
- source: narratio.transcript.trimmed
dest: transcripts/trimmed.json
required: true
`)
fake := &storage.FakeBackend{}
trimmedKey := artifacts.S3PromotedArtifactKey(
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())
}
out := stdout.String()
for _, want := range []string{
for _, unwanted := range []string{
"narratio.transcript.full 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.bounds.session dest=transcripts/bounds.json remote=promoted",
} {
@@ -426,19 +438,28 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) {
"Configured:",
"Previous-session:",
"Promoted:",
"narratio.transcript.trimmed locked",
"narratio.transcript.trimmed locked remote=promoted",
"narratio.transcript.merged remote=missing",
"narratio.transcript.full dest=transcripts/full.json remote=promoted",
} {
if !strings.Contains(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) {
workspaceRoot := t.TempDir()
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")}
var storeInitCalls int
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})