Skip to content
Merged
4 changes: 2 additions & 2 deletions internal/cli/copy_catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func TestCopyCatalog(t *testing.T) {
p.Hintf("For help: https://docs.tracebloc.io/create-use-case/prepare-dataset")
pr := &catalogPrompter{w: &b, answers: answers}
a := &runDataIngestArgs{}
if err := runInteractive(p, pr, a, false /*taskSet*/); err != nil {
if err := runInteractive(p, pr, a); err != nil {
t.Fatalf("driveIngest(%s): %v", dir, err)
}
return strings.ReplaceAll(b.String(), dir, shownPath)
Expand Down Expand Up @@ -211,7 +211,7 @@ func TestCopyCatalog(t *testing.T) {
}
dataIngestFile := doc(
"tb data ingest — stage a dataset into your secure environment",
"What you see when you run `tb data ingest` with no flags: a short intro, a\nfour-step guided setup (intent, name, path, task) then the task-specific\nquestions, and — after you confirm — the run itself. The setup is\ndriven through the real flow for one task in each family (tabular, image, text)\nso the task-specific questions are visible; each core question prints as a\n`Step N of 4 · …` header, the task-specific ones (the label column, and extras\nlike resolution or schema) as their own header, the\nsupporting line beneath it, and the `?` line shows your answer. The run (shown\nonce, for tabular) is the three steps + the final summary as the CLI renders\nthem. Passing flags (--as, --task, a path, …) skips the matching questions. The\nother tasks' extra questions (keypoints, label policy, time column),\nself-supervised text (which skips the label question), and the failure-summary\nwordings are in zz-all-strings.golden. The raw ingestor stream the CLI streams\nthrough (MySQL waits, the 📊 banner, per-validator lines) is the engine's own\nstdout — not CLI copy — so it isn't shown. (`tb ingest` is a hidden deprecated\nalias; `push` is a deprecated alias of the verb.)",
"What you see when you run `tb data ingest` with no flags: a short intro, a\nfour-step guided setup (intent, name, path, task) then the task-specific\nquestions, and — after you confirm — the run itself. The setup is\ndriven through the real flow for one task in each family (tabular, image, text)\nso the task-specific questions are visible; each core question prints as a\n`Step N of 4 · …` header, the task-specific ones (the label column, and extras\nlike resolution or schema) as their own header, the\nsupporting line beneath it, and the `?` line shows your answer. The run (shown\nonce, for tabular) is the three steps + the final summary as the CLI renders\nthem. Values passed as flags (--as, --task, a path, …) pre-fill the matching\nquestions rather than skipping them — guided mode always asks. The\nother tasks' extra questions (keypoints, label policy, time column),\nself-supervised text (which skips the label question), and the failure-summary\nwordings are in zz-all-strings.golden. The raw ingestor stream the CLI streams\nthrough (MySQL waits, the 📊 banner, per-validator lines) is the engine's own\nstdout — not CLI copy — so it isn't shown. (`tb ingest` is a hidden deprecated\nalias; `push` is a deprecated alias of the verb.)",
[]run{
{"tb data ingest # guided · tabular classification", tabularIngest},
{"tb data ingest # guided · image classification", imageIngest},
Expand Down
14 changes: 5 additions & 9 deletions internal/cli/data_ingest_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ Exit codes:
// Dropping --task's old image_classification default means an
// unset task now drives the picker (TTY) or a clear error
// (non-interactive), never a silent image assumption.
taskSet := cmd.Flags().Changed("task") || cmd.Flags().Changed("category")
// Record whether --number-of-keypoints was explicitly passed, so
// the keypoint set-vs-unset message (#76b) can distinguish an
// explicit zero value from an unset flag (both look like the Go
Expand Down Expand Up @@ -240,7 +239,6 @@ Exit codes:
Printer: printer,
Interactive: interactive,
Prompter: pr,
TaskSet: taskSet,
ChangedFlags: changedFlags,
OutputJSON: outputJSON,
JSONOut: jsonOut,
Expand Down Expand Up @@ -337,15 +335,13 @@ type runDataIngestArgs struct {
// the RunE from the persistent --plain flag (see printerFor).
Printer *ui.Printer

// Interactive guided mode (#28). When Interactive is true,
// runDataIngest prompts (via Prompter) for any missing core inputs
// before validation. TaskSet records whether the task was passed
// explicitly (via --task or the hidden --category alias); an unset
// task drives the picker rather than assuming a default. Prompter is
// nil off a TTY / --no-input.
// Interactive guided mode (#28). When Interactive is true, runDataIngest
// walks every question relevant to the chosen task (via Prompter) before
// validation, pre-filling each from whatever arrived on the command line
// (#509). Prompter is nil off a TTY / --no-input, which is what keeps
// scripts flag-only.
Interactive bool
Prompter prompter
TaskSet bool

// ReviewShown records whether the guided flow rendered the pre-confirm
// Review (it only does when it actually prompted for something). It gates
Expand Down
63 changes: 13 additions & 50 deletions internal/cli/data_ingest_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func resolveLocalInput(out, errOut io.Writer, a *runDataIngestArgs) (layout *pus
// validation. Flags already provided win; non-TTY / --no-input
// leaves Prompter nil and skips straight to the flag-only path.
if a.Interactive && a.Prompter != nil {
if err := runInteractive(a.Printer, a.Prompter, a, a.TaskSet); err != nil {
if err := runInteractive(a.Printer, a.Prompter, a); err != nil {
if errors.Is(err, errInteractiveCancelled) {
// cleanCancel prints the shared note and returns the clean exit.
return nil, nil, nil, true, cleanCancel(a.Printer, "nothing was ingested.")
Expand Down Expand Up @@ -201,55 +201,18 @@ func resolveLocalInput(out, errOut io.Writer, a *runDataIngestArgs) (layout *pus
a.Spec.Category, push.SupportedCategoriesList())}
}

// Image-only flags. --target-size / --min-size describe image
// resolution, so they're meaningless on a tabular / text task.
// Reject them explicitly here: without this guard they'd be parsed
// only inside the image branch below, so on a non-image task the
// value — even a malformed one — was silently dropped with no error.
if !push.IsImage(a.Spec.Category) {
for _, f := range []struct{ name, val string }{
{"--target-size", a.TargetSizeFlag},
{"--min-size", a.MinSizeFlag},
} {
if f.val != "" {
return nil, nil, nil, false, &exitError{code: exitBadInput, err: fmt.Errorf(
"%s is image tasks only; it doesn't apply to task %q",
f.name, a.Spec.Category)}
}
}
}

// Task-scoped flags. Like --target-size/--min-size above, each of these is
// read only inside the one category branch that consumes it, so passing one
// on a task that doesn't use it silently dropped the value — and the user's
// intent — with no error, even though the help text says each is scoped.
// Reject a misapplied flag explicitly so it fails fast instead of being
// ignored (the scope mirrors spec.go's build gates exactly).
if a.SchemaFlag != "" && !push.IsTabular(a.Spec.Category) {
return nil, nil, nil, false, &exitError{code: exitBadInput, err: fmt.Errorf(
"--schema is tabular/time-series tasks only; it doesn't apply to task %q", a.Spec.Category)}
}
if a.Spec.LabelPolicy != "" && !push.IsRegressionClass(a.Spec.Category) {
return nil, nil, nil, false, &exitError{code: exitBadInput, err: fmt.Errorf(
"--label-policy is regression-class tasks only (tabular_regression, "+
"time_series_forecasting, time_to_event_prediction); it doesn't apply to task %q",
a.Spec.Category)}
}
if a.Spec.TimeColumn != "" && a.Spec.Category != "time_to_event_prediction" {
return nil, nil, nil, false, &exitError{code: exitBadInput, err: fmt.Errorf(
"--time-column is time_to_event_prediction only; it doesn't apply to task %q", a.Spec.Category)}
}
if a.Spec.NumberOfKeypoints != 0 && a.Spec.Category != "keypoint_detection" {
return nil, nil, nil, false, &exitError{code: exitBadInput, err: fmt.Errorf(
"--number-of-keypoints is keypoint_detection only; it doesn't apply to task %q", a.Spec.Category)}
}
// --label-column is meaningless for self-supervised text (the label is the
// text itself); buildText drops it, so accepting it silently discarded the
// user's value and the review echoed a column that never shipped.
if a.Spec.LabelColumn != "" && push.SelfSupervisedText(a.Spec.Category) {
return nil, nil, nil, false, &exitError{code: exitBadInput, err: fmt.Errorf(
"--label-column doesn't apply to task %q — it trains on the text itself, with no label column",
a.Spec.Category)}
// Task-scoped flags. Each of these is read only inside the one category
// branch that consumes it, so passing one on a task that doesn't use it
// silently dropped the value — and the user's intent — with no error, even
// though the help text says each is scoped. Reject a misapplied flag
// explicitly so it fails fast instead of being ignored (the scope mirrors
// spec.go's build gates exactly).
//
// The scopes themselves live in task_scope.go, shared with the guided
// flow's post-picker reset: both need the same answer to "does this task
// use this value?", and two copies of that answer would drift silently.
if err := rejectMisappliedTaskValues(a); err != nil {
return nil, nil, nil, false, &exitError{code: exitBadInput, err: err}
}

// 3. Walk the local directory FIRST (local "fail fast"), dispatched
Expand Down
Loading
Loading