Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.10.7
0.10.8
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