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
64 changes: 43 additions & 21 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,21 +468,37 @@ make the aside jump on every visit.

### Continue where you left off

The card names a project, a batch, `n / m annotated` in tabular figures, and a picture of
the frame it would open. It holds the view's one filled button.

**It is ranked by progress, not by recency, and that is forced rather than chosen.** There
is no timestamp anywhere on a batch, an annotation, or an asset's progress, so *the batch
I touched last* has no source in the storage format and deriving one would mean a
migration. What the rows can answer is which batch is furthest through and not yet
finished, which is what the card offers. It degrades correctly — one open batch is offered
whatever its progress — and the accepted limit is that two batches part-way through
resolve to the further-along one rather than the more recent.

**The label is load-bearing.** With a frame left to label the control reads **Continue
annotating** and opens the editor at that frame. With none left it reads **Open batch** and
goes to the gallery, because there is no frame to open and a button claiming otherwise
would land somewhere empty. Same card, same slot, different promise.
The card names a project, a batch, a count in tabular figures, and a picture of the frame
it would open. It holds the view's one filled button.

**It is ranked by when somebody last worked the batch.** That is the only work-dating
timestamp in the storage format, and it exists for this. Batches nobody has worked since it
became recordable rank behind every batch that has one, ordered among themselves by how far
through they are — which is the whole population of a workspace created before the stamp
existed, since it was added without a backfill and a moment that was never recorded cannot
be invented. Such a workspace therefore behaves as it did before and converges to real
recency the moment anybody uses it. A batch still has no age of its own: nothing dates its
creation or its state changes, so *the oldest untouched batch* remains underivable.

**The label is load-bearing, and it is the wire that picks it.** The card renders three
promises out of one slot: **Continue annotating** when the batch has a frame nobody has
labeled, **Review annotations** when the labeling is done and frames are waiting on a
reviewer, and **Open batch** when it needs neither and there is no frame to open. The first
two go to the editor and the third to the gallery, because a button claiming a frame that
does not exist would land somewhere empty.

The two editor promises are the same destination, and that is the point rather than a
shortcut: a frame awaiting review opens read-only with **Accept** and **Return to
annotator** on it, so the reviewer and the annotator are one screen wearing what it is
looking at. Under *Review annotations* the count line changes with the label — `k waiting on
review` rather than `n / m annotated`, since how much of the batch is labeled is not the
number anybody came for.

**Which of the three applies is decided by the kernel and carried on the response.** The
order between them is a judgment about what somebody should do next, not a fact the rest of
the response restates, and a judgment spelled once in the kernel and again in the browser is
one that drifts. Contrast the first-run state, which is deliberately *not* a field: that one
is a count the response already carries.

### Needs your attention, and activity

Expand All @@ -500,13 +516,19 @@ project*, not one run finishing, because an ingest records no time at all; and a
reports a version being **created**, because which version is active is derived — it is the
highest — so there is no activation to date.

### The one filled button, in all three states
### The one filled button, in every state

Principle 8 is a count, so Home answers it in every state and there is never a fourth:
first run offers **Create project**, a workspace with somewhere to carry on offers
**Continue annotating**, and one with nothing open offers **New project** — because when
every batch is finished, starting the next piece of work genuinely is what comes next.
When the resume card renders, the header's own New project steps back to `secondary`.
Principle 8 is a count, so Home answers it in every state and there is never a second.
First run offers **Create project**. A workspace with somewhere to carry on offers whichever
of **Continue annotating**, **Review annotations** or **Open batch** the resume card
resolved to, and the header's own New project steps back to `secondary` behind it. Only a
workspace with nothing open at all offers **New project** as the filled one — when every
batch is finished and nothing is waiting on a reviewer, starting the next piece of work
genuinely is what comes next.

That last state is the one to keep honest. It used to absorb a workspace whose only
outstanding work was a review, which meant the page answered "what do I do next?" by
suggesting more work rather than by naming the work already waiting.

The first-run CTA opens the **same dialog** the project list's button opens, rather than
navigating to the screen that carries it. A filled button labelled *Create project* that
Expand Down
24 changes: 18 additions & 6 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,12 +375,24 @@ here to act on, only rows pointing at resources that declare their own capabilit
because the page it answers asks four questions that each span every project, and answering them
as separate resources would be a request per project per question.

Two of its fields are honest about limits the storage format imposes, and a client should render
them as they are described rather than as it might wish them. `resume` is ranked by **progress,
not recency** - nothing records when a batch was last worked on - and a null `next_asset_id` means
the batch has no unlabeled frame left, so the caller opens its gallery rather than the editor. An
`ingest` activity entry is the newest asset arrival in a project rather than one run finishing,
because an ingest job records no times.
**`resume` declares its own kind, and that is the field to read first.** `annotate` means
`next_asset_id` is a frame nobody has labeled, `review` means it is one awaiting a reviewer, and
`open` means the batch is settled throughout and `next_asset_id` is null - so the caller opens its
gallery rather than the editor. The three are in priority order and the order is resolved here: it
is a judgment about what somebody should do next rather than a fact the rest of the response
restates, and a client that worked it out again from the other fields would be keeping a second
copy of a rule that can drift. Contrast the first-run state, which is deliberately *not* a field
because `totals.projects` already answers it.

Batches are ranked by when somebody last worked them - the one work-dating timestamp in the
storage format. Ones nobody has worked since that became recordable rank last, ordered among
themselves by how far through they are, which is every batch in a workspace created before the
stamp existed: it was added without a backfill, because a moment that was never recorded cannot be
reconstructed. Such a workspace behaves as it did before and converges as soon as anybody uses it.

One further field is honest about a limit the storage format still imposes, and a client should
render it as described rather than as it might wish. An `ingest` activity entry is the newest
asset arrival in a project rather than one run finishing, because an ingest job records no times.

## Where the UI lives

Expand Down
5 changes: 3 additions & 2 deletions docs/persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,12 @@ MIGRATIONS: list[Migration] = [
Migration(version=5, name="schema_provenance", upgrade=_add_schema_provenance),
Migration(version=6, name="inference_connections", upgrade=_add_inference_connections),
Migration(version=7, name="model_family", upgrade=_add_model_family),
Migration(version=8, name="progress_touched", upgrade=_add_progress_touched),
]
FORMAT_VERSION: int = MIGRATIONS[-1].version # 7
FORMAT_VERSION: int = MIGRATIONS[-1].version # 8
```

**Generation 1 is the baseline, and the six entries after it are ordinary migrations.** A long
**Generation 1 is the baseline, and the seven entries after it are ordinary migrations.** A long
chain of generations got this schema to its present shape while VisionSet was unreleased.
Every database they could have upgraded was disposable test data inside this repository, so
what they actually bought was an idempotency argument and an undo line per generation, plus
Expand Down
31 changes: 23 additions & 8 deletions docs/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,29 @@ mutation takes it as input; every row deep-links to a resource whose own wire sh
says what may be done to it. A second copy of those declarations here would be the
hand-mirrored table the capabilities contract forbids, one layer up.

Two things about it are consequences of the storage format rather than choices, and
both are stated on the endpoint as well as here. The resume target is ranked by
**progress, not recency** - no timestamp exists on a batch, an annotation or an
asset's progress row, so "most recently worked on" has no source - and when its
`next_asset_id` is null the batch has no unlabeled frame left, which is the client's
signal to open the gallery and to say *Open batch* rather than *Continue annotating*.
The activity feed's `ingest` entry is the newest `Asset.ingested_at` in a project
rather than a run finishing, because an ingest job records no times at all.
**The resume target declares its own kind, and the screen renders it rather than
working it out.** `annotate` means `next_asset_id` is a frame nobody has labeled,
`review` means it is one awaiting a reviewer, and `open` means the batch is settled
throughout and there is no frame at all. The card's label follows - *Continue
annotating*, *Review annotations*, *Open batch* - and so does its destination: the
first two open the editor, the third the gallery. The two editor cases are the same
route because a `review_pending` frame opens read-only with the review actions on it,
which is the position [the annotator section below](#review-is-a-flow-not-an-api-only-edge)
already takes.

The order between the three is resolved on the server. It is a judgment about what
somebody should do next rather than a fact the rest of the response restates, so a
client deriving it again would be keeping a second copy of a rule that can drift -
the shape of defect the capabilities contract exists to prevent. Batches are ranked
by when somebody last worked them, with ones nobody has worked since that became
recordable ranked last and ordered among themselves by progress. That second group
is every batch in a workspace created before the stamp existed, since it was added
without a backfill.

One thing about the page is still a consequence of the storage format rather than a
choice, and it is stated on the endpoint as well as here: the activity feed's
`ingest` entry is the newest `Asset.ingested_at` in a project rather than a run
finishing, because an ingest job records no times at all.

A workspace with no projects reads zeros, nulls and empty lists. That is the
first-run state, and `totals.projects` is how the screen recognises it - not a flag,
Expand Down
2 changes: 1 addition & 1 deletion docs/workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ A missing `blobs/` is repaired rather than rejected: zip archives and git both d
directories, so its absence says nothing about the workspace's health.

**Older workspaces are migrated, not refused** — that is what the migration list is for. It
holds seven entries today: the baseline, and six that have appended a column or a table since.
holds eight entries today: the baseline, and seven that have appended a column or a table since.
A workspace stamped below `FORMAT_VERSION` runs whatever is pending and is restamped, in place
and on the way in. See [persistence.md](persistence.md#migrations-and-format_version) for the
list itself and for the rules a new entry has to satisfy. The honest cost of an in-place
Expand Down
33 changes: 26 additions & 7 deletions frontend/ui-core/src/generated/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,12 +709,18 @@ export interface paths {
* project list, not a copy of it, and `activity` is capped — both have a screen
* that owns them in full.
*
* `resume` is the batch to carry on with: the one furthest through that still
* has an unannotated frame. It is **derived on every call and never stored**,
* and it is ranked by progress rather than by recency because nothing in the
* workspace records when a batch was last worked on. When `next_asset_id` is
* null the batch has no unlabeled frame left — open its gallery rather than the
* editor. `resume` itself is null when no batch is open for annotation.
* `resume` is the batch to carry on with, **derived on every call and never
* stored**. Read its `kind` first: `annotate` means `next_asset_id` is a frame
* nobody has labeled, `review` means it is one awaiting a reviewer, and `open`
* means the batch is settled throughout and `next_asset_id` is null — open its
* gallery rather than the editor. The three are in priority order, decided
* here, and a client renders what it is told rather than working it out again.
* `resume` itself is null when no batch is open for annotation.
*
* Batches are ranked by when somebody last worked them. Ones nobody has worked
* since that became recordable rank last, ordered among themselves by how far
* through they are — which is every batch in a workspace created before the
* stamp existed, since it was added without a backfill.
*
* `attention` carries batches with frames awaiting review, and background jobs
* that failed or are still running. A job row has no `project_id`: a job names
Expand Down Expand Up @@ -3636,9 +3642,19 @@ export interface components {
*/
release_id: string;
};
/**
* ResumeKind
* @description What an open batch is being offered for, and so what `next_asset_id` is.
*
* `annotate` - a frame nobody has labeled, which is that frame.
* `review` - every frame is labeled or set aside and some await a reviewer,
* which is the first of those. `open` - neither, and `next_asset_id` is null.
* @enum {string}
*/
ResumeKind: "annotate" | "review" | "open";
/**
* ResumeTargetOut
* @description The batch to carry on with, and where inside it to land.
* @description The batch to carry on with, what for, and where inside it to land.
*/
ResumeTargetOut: {
/** Annotated */
Expand All @@ -3655,6 +3671,7 @@ export interface components {
* Format: uuid
*/
job_id: string;
kind: components["schemas"]["ResumeKind"];
/** Next Asset Id */
next_asset_id: string | null;
/**
Expand All @@ -3664,6 +3681,8 @@ export interface components {
project_id: string;
/** Project Name */
project_name: string;
/** Review Pending */
review_pending: number;
/** Thumbnail Asset Id */
thumbnail_asset_id: string | null;
/** Total */
Expand Down
5 changes: 4 additions & 1 deletion frontend/ui-core/src/generated/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,11 @@ export const checkAttentionItemOut: Check<Schemas["AttentionItemOut"]> =
export const checkProjectSummaryOut: Check<Schemas["ProjectSummaryOut"]> =
/*#__PURE__*/ object({ "annotated_fraction": [true, isNumber], "asset_count": [true, isInteger], "name": [true, isString], "project_id": [true, isString] } as const);

export const checkResumeKind: Check<Schemas["ResumeKind"]> =
/*#__PURE__*/ oneOf(["annotate", "review", "open"] as const);

export const checkResumeTargetOut: Check<Schemas["ResumeTargetOut"]> =
/*#__PURE__*/ object({ "annotated": [true, isInteger], "batch_id": [true, isString], "batch_name": [true, isString], "job_id": [true, isString], "next_asset_id": [true, either([isString, isNull] as const)], "project_id": [true, isString], "project_name": [true, isString], "thumbnail_asset_id": [true, either([isString, isNull] as const)], "total": [true, isInteger] } as const);
/*#__PURE__*/ object({ "annotated": [true, isInteger], "batch_id": [true, isString], "batch_name": [true, isString], "job_id": [true, isString], "kind": [true, checkResumeKind], "next_asset_id": [true, either([isString, isNull] as const)], "project_id": [true, isString], "project_name": [true, isString], "review_pending": [true, isInteger], "thumbnail_asset_id": [true, either([isString, isNull] as const)], "total": [true, isInteger] } as const);

export const checkWorkspaceTotalsOut: Check<Schemas["WorkspaceTotalsOut"]> =
/*#__PURE__*/ object({ "annotations": [true, isInteger], "assets": [true, isInteger], "projects": [true, isInteger], "releases": [true, isInteger] } as const);
Expand Down
Loading
Loading