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
58 changes: 33 additions & 25 deletions internal/cli/data.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,9 +133,9 @@ func newDataIngestCmd() *cobra.Command {
contextOverride string
nsOverride string

// Ingest-spec flags. image_classification + the tabular /
// time-series family are supported today; text + detection +
// segmentation land in later increments.
// Ingest-spec flags. All schema task categories are CLI-supported now
// (image classification / detection / segmentation / keypoint, the full
// text family, and the tabular / time-series family).
//
// --name/--task are the canonical flags (#180); --table/--category
// stay on as hidden deprecated aliases so existing scripts keep
Expand DownExpand Up@@ -612,12 +612,10 @@ collaborators can train against that table without ever seeing the raw files.`))
// 2. Category gate. Runs BEFORE schema validation so an
// unsupported category gets a clear, actionable CLI message
// rather than the schema's terse enum / missing-property error.
// Supported today: image_classification + the tabular /
// time-series family. The other image categories need sidecar
// (annotation/mask) staging the CLI doesn't do yet, and the
// text family needs a texts/sequences dir — both land in later
// increments. A typo'd category also lands here with a clear
// list rather than the schema's 11-option enum dump.
// Every schema task category is CLI-supported now; this gate stays as
// defensive routing so a future known-but-not-yet-wired category gets a
// clear per-category message, and a typo'd category gets the supported
// list rather than the schema's raw enum dump.
switch {
case a.Spec.Category == "":
// No task chosen. In guided mode the picker already filled this;
Expand All@@ -631,16 +629,19 @@ collaborators can train against that table without ever seeing the raw files.`))
case push.IsCLISupported(a.Spec.Category):
// supported
case push.IsKnown(a.Spec.Category):
// A recognized category data ingest doesn't implement yet — today just
// semantic_segmentation (awaiting the ingestor's mask_id link column +
// training sign-off, backend#816). Routed here (not the default branch)
// so the user gets the registry's per-category pending-support reason,
// A recognized category the CLI doesn't implement yet. None today — every
// schema category is wired — but kept as defensive routing so a future
// known-but-unsupported category gets the registry's per-category reason,
// not a misleading "unrecognized category". Supported categories were
// already caught above, so IsKnown here means known-but-unsupported.
spec, _ := push.Lookup(a.Spec.Category)
reason := ""
if spec.UnsupportedNote != "" {
reason = " (" + spec.UnsupportedNote + ")"
}
return &exitError{code: 2, err: fmt.Errorf(
"task %q isn't supported by the CLI yet (%s). Supported tasks: %s.",
a.Spec.Category, spec.UnsupportedNote, push.SupportedCategoriesList())}
"task %q isn't supported by the CLI yet%s. Supported tasks: %s.",
a.Spec.Category, reason, push.SupportedCategoriesList())}
default:
return &exitError{code: 2, err: fmt.Errorf(
"task %q isn't a recognized task. Supported tasks: %s.",
Expand DownExpand Up@@ -716,6 +717,8 @@ collaborators can train against that table without ever seeing the raw files.`))
layout, err = push.DiscoverText(a.Spec.Category, a.LocalPath)
case a.Spec.Category == "object_detection":
layout, err = push.DiscoverObjectDetection(a.LocalPath)
case a.Spec.Category == "semantic_segmentation":
layout, err = push.DiscoverSemanticSegmentation(a.LocalPath)
default:
// image_classification + keypoint_detection: labels.csv + images/.
layout, err = push.Discover(a.LocalPath)
Expand DownExpand Up@@ -859,16 +862,18 @@ collaborators can train against that table without ever seeing the raw files.`))
// the registry's SelfSupervised flag (not a hardcoded id).
}

// 3b. Friendly missing-label pre-check (#214). Every tabular / time-series
// task carries a label column (layout contract has_label_column=true for
// the whole family). With no --label-column the synthesized spec's
// `label` is an empty string, which trips the schema's label oneOf and
// the raw validation below dumps an opaque "got object, want string" /
// "minLength" pair. Intercept ONLY that specific missing case here — a
// label that's present-but-not-in-the-CSV still flows to
// runLocalPreflight's CheckLabelColumn, and every other schema error
// still reaches the dump — and name the flag to fix instead.
if push.IsTabular(a.Spec.Category) && a.Spec.LabelColumn == "" {
// 3b. Friendly missing-label pre-check (#214). Tabular / time-series tasks
// AND semantic_segmentation carry a required label column (the ingest
// schema's allOf requires `label` for them). With no --label-column the
// synthesized spec's `label` is an empty string, which trips the schema's
// label oneOf and the raw validation below dumps an opaque "got object,
// want string" / "minLength" pair. semseg is especially prone to this —
// its per-image label reads as vestigial beside the pixel masks, so the
// flag is easy to forget. Intercept ONLY that specific missing case here —
// a label present-but-not-in-the-CSV still flows to runLocalPreflight's
// CheckLabelColumn, and every other schema error still reaches the dump —
// and name the flag to fix instead.
if (push.IsTabular(a.Spec.Category) || a.Spec.Category == "semantic_segmentation") && a.Spec.LabelColumn == "" {
msg := "this task needs a label column, but --label-column wasn't set — " +
"pass --label-column with the name of the target column in your data CSV"
if cols := sortedKeys(a.Spec.Schema); len(cols) > 0 {
Expand DownExpand Up@@ -1318,6 +1323,9 @@ func printLocalSummary(p *ui.Printer, layout *push.LocalLayout, spec map[string]
if anns := layout.Sidecars["annotations"]; len(anns) > 0 {
p.Field("annotations", fmt.Sprintf("%d files", len(anns)))
}
if masks := layout.Sidecars["masks"]; len(masks) > 0 {
p.Field("masks", fmt.Sprintf("%d files", len(masks)))
}
}
p.Field("total size", push.HumanBytes(layout.TotalBytes))

Expand Down
45 changes: 9 additions & 36 deletions internal/cli/data_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,15 +87,13 @@ func execDataIngest(t *testing.T, args []string) (exitCode int, stdout, stderr s
// TestDataIngest_UnsupportedCategory_ExitsTwo: the CLI-side category
// gate runs before schema validation so a customer who passes a
// not-yet-supported category gets an actionable message (exit 2)
// rather than the schema's confusing missing-property error. Today's
// supported set is image_classification + the tabular / time-series
// family; the other image categories (which need annotation/mask
// sidecar staging), the text family, and nonsense values are gated
// out here. Bugbot review-on-self caught the missing gate on PR-a.
// rather than the schema's confusing missing-property error. Every schema
// category is CLI-supported now, so only a dead/removed category
// (instance_segmentation) or a nonsense value is gated out here. Bugbot
// review-on-self caught the missing gate on PR-a.
func TestDataIngest_UnsupportedCategory_ExitsTwo(t *testing.T) {
root := imgcLayout(t)
for _, badCategory := range []string{
"semantic_segmentation", // known but pending (awaiting mask_id + training sign-off, backend#816)
"instance_segmentation", // dead — removed from the registry (#1005), now unrecognized
"definitely-not-a-category", // nonsense; gate catches this too
} {
Expand All@@ -114,36 +112,11 @@ func TestDataIngest_UnsupportedCategory_ExitsTwo(t *testing.T) {
}
}

// TestDataIngest_KnownUnsupportedCategory_PendingNote pins the Bugbot fix
// (v0.4.0 RC): a registry-known but CLI-unsupported category
// (semantic_segmentation — the sole remaining one after phase 4) must get the
// registry's pending-support note, not the misleading "isn't a recognized task
// category" message. execDataIngest discards the error and SilenceErrors
// swallows it, so run the command here and inspect the returned error directly.
func TestDataIngest_KnownUnsupportedCategory_PendingNote(t *testing.T) {
root := imgcLayout(t)
rootCmd := NewRootCmd(BuildInfo{Version: "test"})
rootCmd.SetOut(&bytes.Buffer{})
rootCmd.SetErr(&bytes.Buffer{})
rootCmd.SetArgs([]string{"data", "ingest",
"--kubeconfig=/tmp/tracebloc-cli-test-nonexistent-" + t.Name(),
root, "--name=t1", "--task=semantic_segmentation",
"--intent=train", "--label-column=label"})
err := rootCmd.Execute()
if err == nil {
t.Fatal("expected an error for a known-but-unsupported task")
}
if got := ExitCodeFromError(err); got != 2 {
t.Fatalf("exit code = %d, want 2", got)
}
msg := err.Error()
if strings.Contains(msg, "isn't a recognized task") {
t.Errorf("known task misrouted to the unrecognized-task branch:\n%s", msg)
}
if !strings.Contains(msg, "isn't supported by the CLI yet") {
t.Errorf("want the registry pending-support note, got:\n%s", msg)
}
}
// (Removed) TestDataIngest_KnownUnsupportedCategory_PendingNote pinned the
// pending-support routing for a known-but-CLI-unsupported category. Every schema
// category is wired now (#182 closed semantic_segmentation), so there is no such
// category to exercise it; the defensive IsKnown branch in data.go stays for a
// future one.

// TestDataIngest_TraversalTableName_ExitsTwo is the security
// regression pin at the CLI layer. --name=../../etc must be
Expand Down
18 changes: 11 additions & 7 deletions internal/cli/interactive_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -335,10 +335,10 @@ func TestPickTask_FamilyScoped(t *testing.T) {
}
}

// TestPickTask_ImagePending: semantic_segmentation is the sole remaining
// CLI-pending task, so the image picker still renders a greyed "Not yet in the
// CLI" section with its backend#816 reason.
func TestPickTask_ImagePending(t *testing.T) {
// TestPickTask_ImageAllAvailable: after #182 wired semantic_segmentation, every
// image task is available in the CLI, so the image picker lists them all under
// "Available now:" with no greyed "Not yet in the CLI" pending section.
func TestPickTask_ImageAllAvailable(t *testing.T) {
f := &fakePrompter{answers: map[string]string{"Which task?": "Image classification"}}
var buf bytes.Buffer
p := ui.New(&buf, ui.WithColor(false))
Expand All@@ -349,14 +349,18 @@ func TestPickTask_ImagePending(t *testing.T) {
for _, want := range []string{
"Available now:",
"Image classification",
"Not yet in the CLI:",
"semantic_segmentation",
"backend#816", // the UnsupportedNote reason
"Semantic segmentation", // now selectable, no longer pending
} {
if !strings.Contains(out, want) {
t.Errorf("image picker missing %q:\n%s", want, out)
}
}
// No pending section and no stale backend#816 note now that semseg is wired.
for _, unwanted := range []string{"Not yet in the CLI", "backend#816"} {
if strings.Contains(out, unwanted) {
t.Errorf("image picker still shows pending content %q:\n%s", unwanted, out)
}
}
}

// TestPickTask_TabularGloss: the tabular picker shows the survival-analysis
Expand Down
15 changes: 8 additions & 7 deletions internal/push/category.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -132,13 +132,14 @@
Blurb: "label how two texts relate"},
{ID: "embeddings", Family: FamilyText, Label: "Embeddings", CLISupported: true, SelfSupervised: true,
Blurb: "learn vector representations from text pairs"},
// semantic_segmentation stays CLI-pending: di#136 (mask sidecar) shipped,
// but the ingestor doesn't yet populate the mask_id link column the
// contract requires, and the training-side sign-off is tracked in
// backend#816. Wire it once those land (RFC-0002 phase 4 follow-up).
{ID: "semantic_segmentation", Family: FamilyImage, Label: "Semantic segmentation", CLISupported: false, IsClassification: true,
Blurb: "label every pixel in an image",
UnsupportedNote: "schema-recognized; awaiting the ingestor's mask_id link column + training sign-off (backend#816)"},
// semantic_segmentation: images/ + one PNG mask per image in masks/, linked
// by the manifest's mask_id column (backend#816 contract). Wired in RFC-0002
// phase 4 (#182) now that its blockers landed — di#358 shipped the ingestor's
// require-and-enforce mask_id validator (ingestor v0.7.0) and backend#816
// closed. The CLI stages the masks/ sidecar, declares mask_id in the schema,
// and previews the images↔masks (_mask-suffix) pairing + the mask_id contract.
{ID: "semantic_segmentation", Family: FamilyImage, Label: "Semantic segmentation", CLISupported: true, IsClassification: true,
Blurb: "label every pixel in an image"},
}

// categoryByID indexes the registry for O(1) lookup, built once.
Expand DownExpand Up@@ -300,7 +301,7 @@

// allCategoryIDs returns every recognized category id, in registry order.
// Unexported: only the same-package registry test consumes it.
func allCategoryIDs() []string {

Check failure on line 304 in internal/push/category.go

View workflow job for this annotation

GitHub Actions/ Lint

unreachable func: allCategoryIDs
ids := make([]string, 0, len(categoryRegistry))
for _, c := range categoryRegistry {
ids = append(ids, c.ID)
Expand Down
39 changes: 15 additions & 24 deletions internal/push/category_registry_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,42 +38,33 @@ func TestRegistryKnownCategories(t *testing.T) {

func TestSupportedCategories(t *testing.T) {
got := SupportedCategoryIDs()
// RFC-0002 phase 4 wired the 5 text tasks (token/sentence-pair
// classification, causal LM, seq2seq, embeddings) and backend#1054 WS2
// added time_series_classification, so 15 of the 16 categories are
// pushable; only semantic_segmentation remains pending.
if len(got) != 15 {
t.Fatalf("SupportedCategoryIDs() len = %d, want 15: %v", len(got), got)
// RFC-0002 phase 4 wired the last pending category — semantic_segmentation
// (#182; its blockers landed — di#358 shipped the ingestor's mask_id
// require-and-enforce in v0.7.0, and backend#816 closed). So ALL 16 schema
// categories are pushable now, none gated out.
if len(got) != 16 {
t.Fatalf("SupportedCategoryIDs() len = %d, want 16: %v", len(got), got)
}
for _, id := range got {
if !IsCLISupported(id) {
t.Errorf("SupportedCategoryIDs returned %q but IsCLISupported is false", id)
}
}
// semantic_segmentation is the sole known-but-not-yet-pushable category
// (awaiting the ingestor's mask_id link column + training sign-off,
// backend#816); it must stay gated out and explain why.
for _, id := range []string{"semantic_segmentation"} {
if !IsKnown(id) {
t.Errorf("%s should be known", id)
}
if IsCLISupported(id) {
t.Errorf("%s should not be CLI-supported yet", id)
}
if spec, _ := Lookup(id); spec.UnsupportedNote == "" {
t.Errorf("%s should carry an UnsupportedNote", id)
}
}
// The 5 newly-wired text tasks must now be pushable AND carry no stale
// pending note (the picker only greys out categories with a note).
for _, id := range []string{"token_classification", "sentence_pair_classification", "causal_language_modeling", "seq2seq", "embeddings"} {
// Every known category is now CLI-supported AND carries no stale pending
// note (the picker only greys out categories with an UnsupportedNote).
for _, id := range allCategoryIDs() {
if !IsCLISupported(id) {
t.Errorf("%s should be CLI-supported after phase 4", id)
t.Errorf("%s should be CLI-supported (every category is wired now)", id)
}
if spec, _ := Lookup(id); spec.UnsupportedNote != "" {
t.Errorf("%s is supported but still carries an UnsupportedNote: %q", id, spec.UnsupportedNote)
}
}
// semantic_segmentation specifically — the phase-4 finale (#182) — must be
// pushable now, closing out the known-but-pending set.
if !IsCLISupported("semantic_segmentation") {
t.Error("semantic_segmentation should be CLI-supported after #182")
}
}

func TestPredicatesDeriveFromRegistry(t *testing.T) {
Expand Down
50 changes: 50 additions & 0 deletions internal/push/image_extras.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,12 @@ import (
// (Pascal VOC XML).
var xmlExtensions = map[string]struct{}{".xml": {}}

// pngExtensions are the mask file types semantic_segmentation reads. The
// ingestor's FileTypeValidator forces masks to .png (modalities/validators.py
// semantic_segmentation) and the layout contract's masks sidecar globs *.png,
// so the CLI mirrors that here.
var pngExtensions = map[string]struct{}{".png": {}}

// DiscoverObjectDetection validates a local object_detection dataset:
//
// - <root>/labels.csv (required)
Expand DownExpand Up@@ -49,3 +55,47 @@ func DiscoverObjectDetection(rootDir string) (*LocalLayout, error) {
}
return layout, nil
}

// DiscoverSemanticSegmentation validates a local semantic_segmentation dataset:
//
// - <root>/labels.csv (required; must declare + populate a mask_id column)
// - <root>/images/* (required)
// - <root>/masks/*.png (required; one PNG mask per image)
//
// Like object_detection it builds on the image-classification layout
// (labels.csv + images/) and adds a sidecar — here masks/ — via the shared
// sidecar walker, so the existing tar/stream machinery stages masks under
// "masks/". The images↔masks pairing (by the `_mask` filename suffix) and the
// mask_id link-column contract are previewed in preflight (CheckMaskPairing,
// CheckMaskIdColumn), mirroring the ingestor's FilePairingValidator +
// MaskIdColumnValidator (modalities/validators.py, backend#816).
func DiscoverSemanticSegmentation(rootDir string) (*LocalLayout, error) {
layout, err := Discover(rootDir) // labels.csv + images/ (+ caps + symlink guards)
if err != nil {
return nil, err
}

masks, maskBytes, err := discoverSidecarFiles(layout.Root, "masks", pngExtensions)
if err != nil {
return nil, err
}
if len(masks) == 0 {
return nil, fmt.Errorf(
"no .png mask files found in %q. semantic_segmentation expects "+
"<dir>/masks/*.png (one PNG mask per image, named <image>_mask.png).",
filepath.Join(layout.Root, "masks"))
}
if layout.Sidecars == nil {
layout.Sidecars = map[string][]string{}
}
layout.Sidecars["masks"] = masks
layout.TotalBytes += maskBytes

if layout.TotalBytes > MaxTotalBytes {
return nil, fmt.Errorf(
"dataset is %s, exceeds v0.1 cap of %s. For larger datasets, the "+
"cloud-source path is on the v0.2 roadmap (tracebloc/client#147).",
HumanBytes(layout.TotalBytes), HumanBytes(MaxTotalBytes))
}
return layout, nil
}
Loading
Loading