Add the artifact regeneration command
This commit is contained in:
24
docs/cli.md
24
docs/cli.md
@@ -13,6 +13,7 @@ This runs the canonical full pipeline for session `2026-04-04`.
|
|||||||
Top-level commands:
|
Top-level commands:
|
||||||
|
|
||||||
- `run <session_id>`: run all or one contiguous range of the canonical stage order.
|
- `run <session_id>`: run all or one contiguous range of the canonical stage order.
|
||||||
|
- `regenerate-artifacts <session_id>`: force-run extraction through analysis.
|
||||||
- `run-stage <stage> <session_id>`: run one stage.
|
- `run-stage <stage> <session_id>`: run one stage.
|
||||||
- `analyze <session_id>`: force-run analyze.
|
- `analyze <session_id>`: force-run analyze.
|
||||||
- `publish <session_id>`: force-run publish.
|
- `publish <session_id>`: force-run publish.
|
||||||
@@ -95,6 +96,29 @@ Behavior:
|
|||||||
When `--artifacts` is present, the selected range must contain `analyze` or
|
When `--artifacts` is present, the selected range must contain `analyze` or
|
||||||
`publish`. Either consumer is sufficient, including a one-stage range.
|
`publish`. Either consumer is sufficient, including a one-stage range.
|
||||||
|
|
||||||
|
### `regenerate-artifacts`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
narratio regenerate-artifacts <session_id> [--artifacts <name[,name...]>] [...common config flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
Exactly equivalent to:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
narratio run <session_id> --force --from extract --through analyze [caller options]
|
||||||
|
```
|
||||||
|
|
||||||
|
The command always reruns extraction. Analysis rebuilds the selected configured
|
||||||
|
artifacts and any prerequisites required by those targets; without
|
||||||
|
`--artifacts`, it uses the normal default analysis selection. Publish and notify
|
||||||
|
never run. Common session/configuration options and repeatable artifact values
|
||||||
|
pass through unchanged.
|
||||||
|
|
||||||
|
Because the expansion owns `--force`, `--from`, and `--through`, callers cannot
|
||||||
|
supply those options. The shared `run` parser reports them as duplicate
|
||||||
|
singleton flags. The alias has no private execution options or behavior, and
|
||||||
|
runtime diagnostics may identify the operation as `run`.
|
||||||
|
|
||||||
### `run-stage`
|
### `run-stage`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -142,6 +142,20 @@ shared session lifecycle. In particular, render does not require Notarius or
|
|||||||
Scriptorium, extract does not require Scriptorium, and analyze does not require
|
Scriptorium, extract does not require Scriptorium, and analyze does not require
|
||||||
the transcription, Seriatim, Audita, or Notarius adapters.
|
the transcription, Seriatim, Audita, or Notarius adapters.
|
||||||
|
|
||||||
|
For the common post-transcript development loop, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
narratio regenerate-artifacts 2026-04-04
|
||||||
|
narratio regenerate-artifacts 2026-04-04 --artifacts session_recap,player_handout
|
||||||
|
```
|
||||||
|
|
||||||
|
This command is a transparent expansion to a forced bounded `run` from
|
||||||
|
`extract` through `analyze`. Extraction always rebuilds its complete configured
|
||||||
|
bundle. Analysis rebuilds the selected targets and their required analysis
|
||||||
|
prerequisites, or uses the normal default selection when no artifact names are
|
||||||
|
given. The command does not run publish or notify; delivery remains a separate
|
||||||
|
operator action.
|
||||||
|
|
||||||
## Artifact Selection
|
## Artifact Selection
|
||||||
|
|
||||||
`--artifacts` can be used on `run`, `session plan`, `run-stage`, `analyze`, and
|
`--artifacts` can be used on `run`, `session plan`, `run-stage`, `analyze`, and
|
||||||
|
|||||||
@@ -264,6 +264,8 @@ when excluded upstream work cannot support the selected stages.
|
|||||||
|
|
||||||
## Stage 5 — Exact `regenerate-artifacts` Alias
|
## Stage 5 — Exact `regenerate-artifacts` Alias
|
||||||
|
|
||||||
|
**Status: Completed**
|
||||||
|
|
||||||
### Goal
|
### Goal
|
||||||
|
|
||||||
Add the transparent convenience command without creating another orchestration
|
Add the transparent convenience command without creating another orchestration
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var supportedCommands = []string{"run", "run-stage", "analyze", "publish", "clean", "session"}
|
var supportedCommands = []string{"run", "regenerate-artifacts", "run-stage", "analyze", "publish", "clean", "session"}
|
||||||
|
|
||||||
|
var runCommandFn = Run
|
||||||
|
|
||||||
// Execute dispatches CLI commands and returns a process exit code.
|
// Execute dispatches CLI commands and returns a process exit code.
|
||||||
func Execute(args []string, stdout, stderr io.Writer) int {
|
func Execute(args []string, stdout, stderr io.Writer) int {
|
||||||
@@ -23,7 +25,9 @@ func Execute(args []string, stdout, stderr io.Writer) int {
|
|||||||
var err error
|
var err error
|
||||||
switch cmd {
|
switch cmd {
|
||||||
case "run":
|
case "run":
|
||||||
err = Run(ctx, cmdArgs, stdout)
|
err = runCommandFn(ctx, cmdArgs, stdout)
|
||||||
|
case "regenerate-artifacts":
|
||||||
|
err = RegenerateArtifacts(ctx, cmdArgs, stdout)
|
||||||
case "run-stage":
|
case "run-stage":
|
||||||
err = RunStage(ctx, cmdArgs, stdout)
|
err = RunStage(ctx, cmdArgs, stdout)
|
||||||
case "analyze":
|
case "analyze":
|
||||||
|
|||||||
43
internal/app/regenerate_artifacts.go
Normal file
43
internal/app/regenerate_artifacts.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegenerateArtifacts expands the convenience command into its canonical run
|
||||||
|
// invocation. The run command remains the sole owner of parsing and execution.
|
||||||
|
func RegenerateArtifacts(ctx context.Context, args []string, out io.Writer) error {
|
||||||
|
if containsHelpOption(args) {
|
||||||
|
printRegenerateArtifactsHelp(out)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded := make([]string, 0, len(args)+5)
|
||||||
|
if len(args) > 0 {
|
||||||
|
expanded = append(expanded, args[0])
|
||||||
|
args = args[1:]
|
||||||
|
}
|
||||||
|
expanded = append(expanded, "--force", "--from", "extract", "--through", "analyze")
|
||||||
|
expanded = append(expanded, args...)
|
||||||
|
return runCommandFn(ctx, expanded, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsHelpOption(args []string) bool {
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg == "-h" || arg == "--help" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func printRegenerateArtifactsHelp(out io.Writer) {
|
||||||
|
_, _ = fmt.Fprintln(out, "Usage: narratio regenerate-artifacts <session_id> [--artifacts <name[,name...]>] [common config flags]")
|
||||||
|
_, _ = fmt.Fprintln(out)
|
||||||
|
_, _ = fmt.Fprintln(out, "Exactly equivalent to:")
|
||||||
|
_, _ = fmt.Fprintln(out, " narratio run <session_id> --force --from extract --through analyze [caller options]")
|
||||||
|
_, _ = fmt.Fprintln(out)
|
||||||
|
_, _ = fmt.Fprintln(out, "Extraction always runs; selected analysis artifacts and their required prerequisites are rebuilt. Publish and notify never run.")
|
||||||
|
}
|
||||||
106
internal/app/regenerate_artifacts_test.go
Normal file
106
internal/app/regenerate_artifacts_test.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRegenerateArtifactsForwardsExactCanonicalRunArguments(t *testing.T) {
|
||||||
|
original := runCommandFn
|
||||||
|
t.Cleanup(func() { runCommandFn = original })
|
||||||
|
var captured []string
|
||||||
|
runCommandFn = func(_ context.Context, args []string, _ io.Writer) error {
|
||||||
|
captured = append([]string(nil), args...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
code := Execute([]string{
|
||||||
|
"regenerate-artifacts", "2026-05-03",
|
||||||
|
"--artifacts", "session_recap,player_handout",
|
||||||
|
"--artifacts=player_handout",
|
||||||
|
"--config", "pipeline.yml",
|
||||||
|
"--campaign", "sample-campaign",
|
||||||
|
}, io.Discard, io.Discard)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("Execute() code = %d, want 0", code)
|
||||||
|
}
|
||||||
|
want := []string{
|
||||||
|
"2026-05-03", "--force", "--from", "extract", "--through", "analyze",
|
||||||
|
"--artifacts", "session_recap,player_handout",
|
||||||
|
"--artifacts=player_handout",
|
||||||
|
"--config", "pipeline.yml",
|
||||||
|
"--campaign", "sample-campaign",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(captured, want) {
|
||||||
|
t.Fatalf("forwarded args = %#v, want %#v", captured, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegenerateArtifactsHelpDoesNotInvokeRun(t *testing.T) {
|
||||||
|
original := runCommandFn
|
||||||
|
t.Cleanup(func() { runCommandFn = original })
|
||||||
|
called := false
|
||||||
|
runCommandFn = func(_ context.Context, _ []string, _ io.Writer) error {
|
||||||
|
called = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
if code := Execute([]string{"regenerate-artifacts", "--help"}, &stdout, &stderr); code != 0 {
|
||||||
|
t.Fatalf("Execute() code = %d, stderr = %q", code, stderr.String())
|
||||||
|
}
|
||||||
|
if called {
|
||||||
|
t.Fatal("help invoked canonical run handler")
|
||||||
|
}
|
||||||
|
for _, detail := range []string{
|
||||||
|
"narratio run <session_id> --force --from extract --through analyze",
|
||||||
|
"Extraction always runs",
|
||||||
|
"Publish and notify never run",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(stdout.String(), detail) {
|
||||||
|
t.Fatalf("help = %q, want %q", stdout.String(), detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stderr.Len() != 0 {
|
||||||
|
t.Fatalf("stderr = %q, want empty", stderr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegenerateArtifactsOwnedOptionsFailThroughRunParser(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
owned string
|
||||||
|
}{
|
||||||
|
{name: "force", args: []string{"--force"}, owned: "force"},
|
||||||
|
{name: "from", args: []string{"--from=render"}, owned: "from"},
|
||||||
|
{name: "through", args: []string{"--through", "publish"}, owned: "through"},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
args := []string{"regenerate-artifacts", "2026-05-03"}
|
||||||
|
args = append(args, test.args...)
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
if code := Execute(args, io.Discard, &stderr); code == 0 {
|
||||||
|
t.Fatalf("Execute(%#v) code = 0", args)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr.String(), "--"+test.owned+" may be specified only once") {
|
||||||
|
t.Fatalf("stderr = %q, want shared duplicate %s error", stderr.String(), test.owned)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegenerateArtifactsRejectsUnknownOptionsThroughRunParser(t *testing.T) {
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
if code := Execute([]string{"regenerate-artifacts", "2026-05-03", "--regenerate-only"}, io.Discard, &stderr); code == 0 {
|
||||||
|
t.Fatal("Execute() code = 0")
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr.String(), "flag provided but not defined") || !strings.Contains(stderr.String(), "regenerate-only") {
|
||||||
|
t.Fatalf("stderr = %q, want canonical parser unknown-option error", stderr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user