Skip to content

Correction-batch UI: the end of the forward-only story (G6) - #313

Merged
JArmandoAnaya merged 2 commits into
mainfrom
feat/t10-correction-batch-ui
Aug 4, 2026
Merged

Correction-batch UI: the end of the forward-only story (G6)#313
JArmandoAnaya merged 2 commits into
mainfrom
feat/t10-correction-batch-ui

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

Task 10 — the last of the 2026-08 checkpoint audit remediation. Closes G6.

What this finishes

A completed batch is immutable as a workflow unit — the kernel gives it no exit and none is coming. The product's answer to "this frame is wrong" had been a dead end dressed three different ways:

Surface Was Now
Annotator on a completed batch fully editable, every save 409'd viewer (#306) → links to the correction
Gallery bulk bar buttons live, every request 409'd disabled with a reason (#305) → "Create one"
The reason itself "corrections happen in a correction batch" …and now one can be made

Every one of those sentences named the route onward deliberately, on the grounds that naming it beats a friendlier lie. This is what they were waiting for.

Where the control lives, and where it does not

Gallery header and Batches row, both capability-gated on create_correction. The gallery takes the current selection, so "these three are bad" is one press rather than a second pass inside the new batch. The Batches row puts it beside Promote rather than in an overflow menu — a completed batch has exactly two things left to do, and hiding the second hides the one somebody is hunting for.

The annotator links rather than duplicating. Creating a batch is a curation act, curation lives on the batch view, and a second place batches are made is a second place the rules can drift. The annotator says which way is forward and hands the person to the screen that owns it, with the batch in view.

Scope, and the option that is deliberately absent

all is the default and sends no asset_ids at all — the server's own default is the parent's whole membership, and re-listing forty-eight ids to say so would be the client telling the API something it already knows. selection is the other ordinary ask, offered only when there is one: a scope choice whose second option covers nothing is a choice between doing something and doing nothing.

No "filtered set" option. The gallery's segments are a view, and a correction cut from whatever happens to be filtered at the moment of pressing is a batch nobody can describe afterwards — where a selection is a thing somebody chose. The task listed it; I left it out for that reason.

Lineage

One hop, on both sides. CorrectionOf says what this batch corrects — not how deep the chain goes, which is the honest reading of the one field there is. It renders nothing without a parent, because "not a correction of anything" is most batches and a badge saying so on every one would be noise on the many to inform the few.

Both directions are derived from the project's batch listing rather than fetched: how many corrections point at a batch and what it points at are a filter and a lookup over one array the screen's siblings already request.

Decisions taken autonomously

  • No "filtered set" scope (above).
  • The annotator links to the gallery instead of hosting its own dialog.
  • The gallery owns the dialog's open state; CorrectionButton is controlled when given open, uncontrolled otherwise — that screen has two ways in, and two independent dialogs would be two states that can both be true.
  • Lineage derived, not fetched — a dedicated read would be a second source for something already on screen.

Found, not fixed

Test plan

All four suites:

  • bash scripts/check.shgreen.
  • CI=1 npx playwright test176 green.
  • CI=1 npx playwright test -c playwright.cycle.config.tsgreen, now including the full correction round-trip against a real server: complete → correct → land on the child with lineage rendered → return and confirm the parent is untouched, including its trunk count.
  • vitest 500 (was 489).

The IA skill's sitemap gains the rule about where a correction is reached from, per its process requirement.

Audit gap G6, and the end of the forward-only story.

A `completed` batch is immutable as a workflow unit — the kernel gives it no exit
and none is coming. The product's answer to "this frame is wrong" had been a dead
end dressed three different ways: an annotator that opened fully editable and
refused every save (#306 made it a viewer), a bulk bar whose buttons were live
and whose every request 409'd (#305 disabled them with a reason), and a sentence
naming a correction batch that nothing could create.

Each of those named the route onward deliberately, on the grounds that it beats a
friendlier lie. This is what they were waiting for.

**Scope is a choice and the default is the whole batch.** "Correct this batch" is
the ordinary ask, so `all` sends no `asset_ids` at all — the server's own default
is the parent's whole membership, and re-listing forty-eight ids to say so would
be the client telling the API something it already knows. `selection` is the
other ordinary ask, and it is offered only when there *is* one: a scope choice
whose second option covers nothing is a choice between doing something and doing
nothing.

There is deliberately no "filtered set" option. The gallery's segments are a
*view*, and a correction cut from whatever happens to be filtered at the moment
of pressing is a batch nobody can describe afterwards — where a selection is a
thing somebody chose.

`defaultCorrectionName` is pure and exported because it is the part with a
decision in it: a dialog that opened blank would make the common case cost a
sentence of typing, and numbering keeps a chain readable in a listing. The count
comes from the caller, which is already holding the project's batch listing.

`CorrectionOf` renders lineage as one hop — what this batch corrects, not how
deep the chain goes, which is the honest reading of the one field there is. It
renders nothing without a parent: "not a correction of anything" is most batches,
and a badge saying so on every one would be noise on the many to inform the few.
…es one

Four surfaces, and the point is that they agree.

**Gallery header** — the screen somebody is on when they find the frame that is
wrong. It takes the current selection, so "these three are bad" is one press
rather than a second pass inside the new batch.

**Batches row** — beside Promote rather than in an overflow menu, because a
completed batch has exactly two things left to do and hiding the second is hiding
the one somebody is hunting for. The row also renders what it corrects, so a
chain stays readable when the table is sorted by something else.

**The annotator's read-only banner** links to the gallery rather than opening a
dialog, and that is a product call: creating a batch is a curation act, curation
lives on the batch view, and a second place batches are made is a second place
the rules can drift. The annotator says which way is forward and hands the person
to the screen that owns it.

**The bulk bar's withheld sentence** gains "Create one", and the selection it is
already holding becomes the correction's scope.

The gallery holds the dialog's open state rather than `CorrectionButton` holding
its own, because that screen has two ways in and two independent dialogs would be
two states that can both be true. `CorrectionButton` is controlled when a caller
supplies `open`, uncontrolled otherwise — which keeps the Batches row a one-liner.

Lineage on both sides is derived from the project's batch listing rather than
fetched: how many corrections point at this batch and what this batch points at
are a filter and a lookup over one array the screen's siblings already request.

## Tests

Twelve vitest cases for the control and its lineage — the two scopes and exactly
what each sends, the selection scope absent when nothing is selected, navigation
to the child, a refusal rendered as prose with the dialog staying open (one a
dialog closes over is one nobody reads), and the suggested name.

The round-trip runs in `cycle.spec.ts` against a **real server**, because the two
claims worth making are about the kernel rather than about a stub: that the
parent is genuinely untouched after a correction is cut, and that the child is a
draft carrying the parent's name in its lineage. It re-checks the parent's trunk
count afterwards, so "untouched" means what it says rather than only "still
completed".

Both fixture modules gain `create_correction` on `completed`, because that is what
the server declares — a mock withholding it would describe a batch the API cannot
produce. `capabilities.test.ts`'s "names every action the wire has" guard caught
the missing constant, which is the job it was written for.

One recorder fix: the body-capturing stub in `promote.test.tsx` called
`JSON.parse` on every non-GET, and `promote` sends no body — so an empty string
threw and failed every test in the file for a reason none of them was about.
@JArmandoAnaya
JArmandoAnaya enabled auto-merge (squash) August 4, 2026 23:45
@JArmandoAnaya
JArmandoAnaya merged commit 381acc8 into main Aug 4, 2026
14 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the feat/t10-correction-batch-ui branch August 4, 2026 23:52
JArmandoAnaya added a commit that referenced this pull request Aug 21, 2026
* feat(ui-core): a finished batch can be corrected

Audit gap G6, and the end of the forward-only story.

A `completed` batch is immutable as a workflow unit — the kernel gives it no exit
and none is coming. The product's answer to "this frame is wrong" had been a dead
end dressed three different ways: an annotator that opened fully editable and
refused every save (#306 made it a viewer), a bulk bar whose buttons were live
and whose every request 409'd (#305 disabled them with a reason), and a sentence
naming a correction batch that nothing could create.

Each of those named the route onward deliberately, on the grounds that it beats a
friendlier lie. This is what they were waiting for.

**Scope is a choice and the default is the whole batch.** "Correct this batch" is
the ordinary ask, so `all` sends no `asset_ids` at all — the server's own default
is the parent's whole membership, and re-listing forty-eight ids to say so would
be the client telling the API something it already knows. `selection` is the
other ordinary ask, and it is offered only when there *is* one: a scope choice
whose second option covers nothing is a choice between doing something and doing
nothing.

There is deliberately no "filtered set" option. The gallery's segments are a
*view*, and a correction cut from whatever happens to be filtered at the moment
of pressing is a batch nobody can describe afterwards — where a selection is a
thing somebody chose.

`defaultCorrectionName` is pure and exported because it is the part with a
decision in it: a dialog that opened blank would make the common case cost a
sentence of typing, and numbering keeps a chain readable in a listing. The count
comes from the caller, which is already holding the project's batch listing.

`CorrectionOf` renders lineage as one hop — what this batch corrects, not how
deep the chain goes, which is the honest reading of the one field there is. It
renders nothing without a parent: "not a correction of anything" is most batches,
and a badge saying so on every one would be noise on the many to inform the few.

* feat(ui-core): every sentence that named a correction batch now reaches one

Four surfaces, and the point is that they agree.

**Gallery header** — the screen somebody is on when they find the frame that is
wrong. It takes the current selection, so "these three are bad" is one press
rather than a second pass inside the new batch.

**Batches row** — beside Promote rather than in an overflow menu, because a
completed batch has exactly two things left to do and hiding the second is hiding
the one somebody is hunting for. The row also renders what it corrects, so a
chain stays readable when the table is sorted by something else.

**The annotator's read-only banner** links to the gallery rather than opening a
dialog, and that is a product call: creating a batch is a curation act, curation
lives on the batch view, and a second place batches are made is a second place
the rules can drift. The annotator says which way is forward and hands the person
to the screen that owns it.

**The bulk bar's withheld sentence** gains "Create one", and the selection it is
already holding becomes the correction's scope.

The gallery holds the dialog's open state rather than `CorrectionButton` holding
its own, because that screen has two ways in and two independent dialogs would be
two states that can both be true. `CorrectionButton` is controlled when a caller
supplies `open`, uncontrolled otherwise — which keeps the Batches row a one-liner.

Lineage on both sides is derived from the project's batch listing rather than
fetched: how many corrections point at this batch and what this batch points at
are a filter and a lookup over one array the screen's siblings already request.

## Tests

Twelve vitest cases for the control and its lineage — the two scopes and exactly
what each sends, the selection scope absent when nothing is selected, navigation
to the child, a refusal rendered as prose with the dialog staying open (one a
dialog closes over is one nobody reads), and the suggested name.

The round-trip runs in `cycle.spec.ts` against a **real server**, because the two
claims worth making are about the kernel rather than about a stub: that the
parent is genuinely untouched after a correction is cut, and that the child is a
draft carrying the parent's name in its lineage. It re-checks the parent's trunk
count afterwards, so "untouched" means what it says rather than only "still
completed".

Both fixture modules gain `create_correction` on `completed`, because that is what
the server declares — a mock withholding it would describe a batch the API cannot
produce. `capabilities.test.ts`'s "names every action the wire has" guard caught
the missing constant, which is the job it was written for.

One recorder fix: the body-capturing stub in `promote.test.tsx` called
`JSON.parse` on every non-GET, and `promote` sends no body — so an empty string
threw and failed every test in the file for a reason none of them was about.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant