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
21 changes: 21 additions & 0 deletions .github/pull_request_template.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
## Summary
<!-- 1–3 sentences. What does this PR do and why? -->

## Related
<!-- Closes #123 / Ref tracebloc/other-repo#456 -->

## Type of change
- [ ] Feature
- [ ] Bug fix
- [ ] Tech-debt / refactor
- [ ] Docs
- [ ] Security / hardening

## Test plan
<!-- Commands run, manual steps. -->

## Checklist
- [ ] Tests added / updated and passing locally
- [ ] `go build ./...`, `go vet`, and the Lint job's checks pass locally
- [ ] Terminal output follows [STYLE.md](../STYLE.md) — Printer tones (no hardcoded colour/emoji), "secure environment" not "workspace"; `bash scripts/check-style.sh` passes
- [ ] No secrets / credentials in the diff
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,6 +51,7 @@ jobs:
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck dash
shellcheck --shell=sh --severity=error scripts/install.sh
shellcheck --shell=bash --severity=error scripts/check-style.sh
dash -n scripts/install.sh
bash -n scripts/tests/install-verify.sh
- name: Verification harness (mandatory cosign / fail-closed)
Expand DownExpand Up@@ -180,6 +181,13 @@ jobs:
# 1500-line data.go from growing quietly (backend#1106 WS-B).
run: ./scripts/file-budget.sh

- name: Style + terminology guard
# Enforces the terminal style system (STYLE.md): no hardcoded brand colour
# outside the tone engine (internal/ui), no status/traffic-light emoji, and
# "secure environment" not "workspace" in user-facing text. Mechanical
# checks only — role/wording judgement stays with review.
run: bash scripts/check-style.sh
Comment thread
cursor[bot] marked this conversation as resolved.

govulncheck:
timeout-minutes: 10
name: govulncheck
Expand Down
7 changes: 7 additions & 0 deletions CODEOWNERS
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,3 +6,10 @@
# CI + release infrastructure. Changes here can break the release
# pipeline; route them to platform reviewers explicitly.
/.github/ @tracebloc/maintainers

# Style-system enforcement (STYLE.md). The guard, the style reference, and the
# pinned brand values decide what output regressions CI blocks; a change here
# changes the rules themselves, so it must be code-owner reviewed.
/scripts/check-style.sh @tracebloc/maintainers
/STYLE.md @tracebloc/maintainers
/internal/ui/brand_tones_test.go @tracebloc/maintainers
10 changes: 9 additions & 1 deletion Makefile
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,7 @@ GOIMPORTS_VERSION ?= v0.48.0
# ---- top-level targets -------------------------------------------

.PHONY: ci
ci: vet test lint fmt-check schema-check vulncheck file-budget deadcode
ci: vet test lint fmt-check schema-check vulncheck file-budget deadcode check-style
@echo "==> ci: all green"

.PHONY: build
Expand DownExpand Up@@ -176,6 +176,14 @@ schema-sync:
file-budget:
./scripts/file-budget.sh

# check-style: enforce the terminal style system + terminology (STYLE.md) —
# no hardcoded brand colour outside internal/ui, no status emoji, "secure
# environment" not "workspace". Mirrors the Lint job's guard so `make ci`
# matches CI. Mechanical only; role/wording judgement stays with review.
.PHONY: check-style
check-style:
./scripts/check-style.sh

# ---- cleanup -----------------------------------------------------

.PHONY: clean
Expand Down
70 changes: 70 additions & 0 deletions STYLE.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
# Terminal output style

The `tracebloc` CLI and the installer share **one** terminal style system. This
is the reference; `scripts/check-style.sh` enforces the mechanical parts in CI.

## The idea

From the tracebloc.io homepage gradient — **cyan orients, lime moves**:

- **cyan** `#01a5cc` = *structure* — headings, step titles, links, "where you are"
- **lime** `#91e947` = *action* — commands, the primary CTA, "what to do next"

Everything else stays quiet (dim neutral). Colour is never load-bearing: headings
and commands also carry **bold**, and alerts carry a distinct **glyph**, so the
output still reads under `NO_COLOR`, in a pipe, or for a colour-blind reader.

## Roles → tones

All colour goes through the `Printer` in `internal/ui` — never inline an escape
or hex elsewhere. The tone table (`internal/ui/ui.go`) maps each role:

| Role | Printer method / tone | Colour | Weight / glyph |
|------|-----------------------|--------|----------------|
| Heading / section / step | `Section`, `Step`, `Banner` (`toneHeading`) | cyan `#01a5cc` | bold |
| Command (to run) | `Command`, `MenuRow` cmd (`toneCommand`) | lime `#91e947` | bold |
| Description / supporting | `MenuRow` desc (`toneDesc`) | soft lime `#a7ed6c` | — |
| Link / URL | `toneLink` | cyan `#01a5cc` | underline |
| Success ✔ / online ● | `Successf`, `CheckLine` (`toneGo`) | lime `#91e947` | glyph |
| Warning ⚠ | `Warnf`, `WarnLine` (`toneWarn`) | amber `#ffc62b` | glyph |
| Error ✖ | `Errorf` (`toneErr`) | red `#f64c4c` | bold glyph |
| Label : value | `Field`, `Stat` (`toneLabel`) | dim neutral | — |

**No emoji.** The lime `●` is the online indicator (not 🟢).

The engine renders exact 24-bit hex on truecolor terminals, the **deep shade**
(`#01637a` / `#578c2b`) on light backgrounds, the nearest ANSI-16 otherwise, and
nothing when colour is off (`NO_COLOR` / non-TTY / `TERM=dumb` / `--plain`). The
exact brand SGR is pinned by `internal/ui/brand_tones_test.go`, so a drift in the
tone table fails CI. The installer mirrors this in `scripts/lib/common.sh`.

## Terminology

Source of truth: the docs repo `TERMINOLOGY.md`. In user-facing output:

| Use | Not |
|-----|-----|
| secure environment | workspace, hub, client (as a noun for the environment) |
| ingest | upload, import |
| delete | remove, uninstall (for offboarding) |
| Online / Offline | connected / disconnected, up / down |
| collaborators | users, members |
| task | job, experiment type |

`client` stays valid as the CLI verb (`tracebloc client create`) and in code
identifiers (`exitNoWorkspace`, etc.) — the guard matches `workspace` as a whole
word in output text only.

## What's enforced vs reviewed

`scripts/check-style.sh` (CI Lint job, blocking) catches the **mechanical**
violations: hardcoded brand colour outside `internal/ui`, status emoji, and
`workspace` in user-facing text. Run it locally with `make check-style` (also part
of `make ci`) or directly: `bash scripts/check-style.sh`.

It can't police **judgement** — using the right *role* for a token (a command in
the command tone, not the heading tone), or the softer terminology calls. Those
stay with review; `STYLE.md` and `scripts/check-style.sh` are CODEOWNER-gated so
the rules can't be quietly weakened.

To intentionally exempt a line, append `// style-guard: allow` with a reason.
14 changes: 7 additions & 7 deletions internal/cli/data.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,11 +25,11 @@ func newDataCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "data",
Aliases: []string{"dataset"},
Short: "Manage the datasets in your workspace",
Long: `Commands for ingesting and managing the datasets your workspace holds —
Short: "Manage the datasets in your secure environment",
Long: `Commands for ingesting and managing the datasets your secure environment holds —
the data models train on. It stays on your infrastructure.

` + "`data ingest`" + ` ingests a local dataset into your workspace's storage,
` + "`data ingest`" + ` ingests a local dataset into your secure environment's storage,
submits the ingestion run, and watches it to completion (streaming
logs + the final summary). ` + "`data validate`" + ` checks an ingest.yaml
locally first.
Expand DownExpand Up@@ -147,8 +147,8 @@ func runDataIngest(ctx context.Context, out, errOut io.Writer, a runDataIngestAr
// live-only steps (stage + ingest) the customer just skipped.
if a.DryRun {
a.Printer.Newline()
a.Printer.Successf("Dry-run complete — your data and workspace check out; nothing was created.")
a.Printer.Hintf("A real run continues with step 2 (copy into your workspace) and step 3 (validate and load).")
a.Printer.Successf("Dry-run complete — your data and secure environment check out; nothing was created.")
a.Printer.Hintf("A real run continues with step 2 (copy into your secure environment) and step 3 (validate and load).")
if a.OutputJSON {
writePushJSON(a.JSONOut, "dry-run", spec, nil, "", "")
jsonEmitted = true
Expand DownExpand Up@@ -196,8 +196,8 @@ func runDataIngest(ctx context.Context, out, errOut io.Writer, a runDataIngestAr
// Exit code 7 ("staging failed") is distinct from the
// pre-flight codes so customers can branch on whether the
// failure was their environment vs the actual data transfer.
a.Printer.Step(2, 3, "Copy into your workspace")
a.Printer.Hintf("Your files are copied securely into your workspace's storage — set up and cleaned up for you.")
a.Printer.Step(2, 3, "Copy into your secure environment")
a.Printer.Hintf("Your files are copied securely into your secure environment's storage — set up and cleaned up for you.")
progress := push.NewProgress(out, layout.TotalBytes,
fmt.Sprintf("Copying %s", a.Spec.Table))
// Defer Finish so a failure path that returns BEFORE
Expand Down
12 changes: 6 additions & 6 deletions internal/cli/data_ingest_cluster.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ func connectIngestTarget(ctx context.Context, a *runDataIngestArgs) (target *clu
// Errors mirror that command's exit-code contract (3 for
// kubeconfig, 4 for missing release) so behaviour is
// consistent across pre-flight commands.
// Connecting to the workspace + discovering its shared storage is
// Connecting to the secure environment + discovering its shared storage is
// Kubernetes plumbing (release / PVC / jobs-manager) the happy path keeps
// quiet — it's no longer a numbered step (RFC-0002 §6), and --verbose adds
// the release/PVC detail below. But the discovery itself is several blocking
Expand All@@ -48,7 +48,7 @@ func connectIngestTarget(ctx context.Context, a *runDataIngestArgs) (target *clu
// namespace-fallback note mid-call, and a spinner's \r redraw would clobber
// it. ALL the logic below (discovery + the exit-6 destination guard) is
// unchanged; only the presentation moved.
a.Printer.Infof("Connecting to your workspace…")
a.Printer.Infof("Connecting to your secure environment…")
// 6. PVC discovery (needPVC) confirms the chart's shared-data PVC is
// Bound before we waste time provisioning a Pod that can't mount it.
opts := cluster.KubeconfigOptions{Path: a.Kubeconfig, Context: a.Context, Namespace: a.Namespace}
Expand DownExpand Up@@ -127,7 +127,7 @@ func runIngestionRun(ctx context.Context, out io.Writer, a runDataIngestArgs, ta
// The chart's helm flow uses the same token-mint code path.
a.Printer.Step(3, 3, "Validate and load")
if a.Detach {
a.Printer.Hintf("Submitting the run — with --detach it keeps running on your workspace after this command returns; the reconnect command is shown below.")
a.Printer.Hintf("Submitting the run — with --detach it keeps running on your secure environment after this command returns; the reconnect command is shown below.")
} else {
a.Printer.Hintf("Submitting the run, then following along as tracebloc validates your data and loads it into the table — progress streams below.")
a.Printer.Hintf("This follows the run for up to an hour; a longer run keeps going on its own (or start it with --detach and check back later).")
Expand All@@ -150,7 +150,7 @@ func runIngestionRun(ctx context.Context, out io.Writer, a runDataIngestArgs, ta
// silent (RFC-0002 "progress on every wait"). The submit POST itself is a
// separate ~30s synchronous wait; its spinner lives in submit.Run, next to
// the POST it covers.
connectSpin := a.Printer.Spinner("Connecting to your workspace to submit the run", "")
connectSpin := a.Printer.Spinner("Connecting to your secure environment to submit the run", "")
pf, err := portForwardJobsManagerFn(ctx, cs, resolved.RestConfig,
resolved.Namespace, release.JobsManagerServiceName, release.JobsManagerPort)
connectSpin.Stop()
Expand DownExpand Up@@ -258,7 +258,7 @@ func shouldReclaimStaging(status string) bool {
return status == "succeeded"
}

// printClusterSummary shows the discovered workspace target. It's Kubernetes
// printClusterSummary shows the discovered secure environment target. It's Kubernetes
// plumbing (release / jobs-manager / shared PVC) the happy path hides, so the
// whole block — header, fields, and the RWO-PVC note — prints only under
// --verbose (RFC-0002 §6). Discovery + guards are unchanged; this is
Expand DownExpand Up@@ -349,7 +349,7 @@ func existingTableAction(a *runDataIngestArgs, existingTable string) (proceed bo
return ok, nil
}
return false, &exitError{code: exitTableExists, err: fmt.Errorf(
"table %q already exists in this workspace. Re-ingesting the same table doesn't merge or replace — "+
"table %q already exists in this secure environment. Re-ingesting the same table doesn't merge or replace — "+
"the run would fail after uploading everything. Re-run with --overwrite to replace it, "+
"or pick a different --name. (`tracebloc data delete %s` also removes it.)",
existingTable, existingTable)}
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/data_ingest_cmd.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,14 +93,14 @@ func newDataIngestCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "ingest <dataset>",
Aliases: []string{"push"},
Short: "Ingest a local dataset into your workspace",
Short: "Ingest a local dataset into your secure environment",
// The task COUNT and the text-family subdir names are derived from the
// registry / vendored layout contract (push.SupportedCategoryIDs +
// push.TextSidecarDir) rather than hardcoded, so the help can't drift
// from what the CLI actually supports (cli#215): the count used to read
// a stale "9", and the text example showed texts/ for every text task
// even though masked_language_modeling stages into sequences/.
Long: fmt.Sprintf(`Ingests a local dataset into your workspace's storage,
Long: fmt.Sprintf(`Ingests a local dataset into your secure environment's storage,
submits the ingestion run, and follows it to completion (streaming
progress + the final summary). Your data never leaves your own
infrastructure. Supports %[1]d tasks across the image, text, and
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/data_ingest_local.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,7 +95,7 @@ func resolveLocalInput(out, errOut io.Writer, a *runDataIngestArgs) (layout *pus
a.Printer.Banner("tracebloc", "data ingest")
a.Printer.Para(strings.TrimSpace(`
This ingests a dataset so models can train on it. Your files never leave your
own infrastructure — tracebloc copies them into your workspace's storage,
own infrastructure — tracebloc copies them into your secure environment's storage,
checks them, and loads them into a table your training runs read from. Other
collaborators can train against that table without ever seeing the raw files.`))
a.Printer.Hintf("Learn more: https://docs.tracebloc.io")
Expand DownExpand Up@@ -285,7 +285,7 @@ collaborators can train against that table without ever seeing the raw files.`))
}

a.Printer.Step(1, 3, "Check your data")
a.Printer.Hintf("Reading your files locally first — nothing has touched your workspace yet — so a layout or settings problem shows up right away.")
a.Printer.Hintf("Reading your files locally first — nothing has touched your secure environment yet — so a layout or settings problem shows up right away.")

// 3a. Per-category spec resolution from the local data, so the
// synthesized spec carries the right fields before validation.
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/ingestion_run_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -204,7 +204,7 @@ func TestRunIngestionRun_SubmitConnectUsesSpinner(t *testing.T) {
t.Fatalf("runIngestionRun: %v", err)
}
out := buf.String()
if !strings.Contains(out, "Connecting to your workspace to submit the run") {
if !strings.Contains(out, "Connecting to your secure environment to submit the run") {
t.Errorf("submit-connect wait is missing its status message:\n%q", out)
}
// A spinner redraws/clears its line with a carriage return; a plain Infof
Expand Down
2 changes: 1 addition & 1 deletion internal/push/stage.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ func Stage(ctx context.Context, opts StageOptions) error {
if err != nil {
return err
}
_, _ = fmt.Fprintf(opts.Out, "Opened a secure channel to your workspace's storage.\n")
_, _ = fmt.Fprintf(opts.Out, "Opened a secure channel to your secure environment's storage.\n")

// 3. Defer cleanup. The deferred call uses a FRESH context with
// its own deadline — if the parent ctx is cancelled (SIGINT,
Expand Down
6 changes: 3 additions & 3 deletions internal/submit/submit.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -147,7 +147,7 @@ func Run(ctx context.Context, opts Options) (*Result, error) {
// --detach: report and bail. The run continues in the cluster;
// there is no CLI re-attach verb yet, so the honest way back is
// the raw log follow, offered as a labelled command.
p.Infof("Detached — the ingestion runs in the background on your workspace.")
p.Infof("Detached — the ingestion runs in the background on your secure environment.")
p.Hintf("Follow it later with: kubectl logs -f -n %s job/%s", resp.Namespace, resp.JobName)
return &Result{Submit: resp}, nil
}
Expand DownExpand Up@@ -175,14 +175,14 @@ func Run(ctx context.Context, opts Options) (*Result, error) {
p.Newline()
switch wr.DetachReason {
case DetachReasonSignal:
p.Infof("Stopped watching — the ingestion keeps running on your workspace.")
p.Infof("Stopped watching — the ingestion keeps running on your secure environment.")
case DetachReasonPodWaitTimeout:
p.Infof("The ingestion hasn't started yet (usually a slow image pull or a busy cluster). " +
"It's queued to run once the cluster can schedule it — check on it with the command below.")
case DetachReasonWatchCap:
p.Infof("Stopped following after 1 hour — the ingestion is still running and will finish on its own.")
default:
p.Infof("Stopped watching — the ingestion keeps running on your workspace.")
p.Infof("Stopped watching — the ingestion keeps running on your secure environment.")
}
p.Hintf("Check on it later with: kubectl logs -f -n %s job/%s", resp.Namespace, resp.JobName)
return &Result{Submit: resp, Watch: wr}, nil
Expand Down
Loading
Loading