Skip to content

fix(progress) + feat(insights): eight defects, and a conclusion you can remove - #602

Merged
mrviduus merged 5 commits into
mainfrom
fix/progress-path-debt
Sep 11, 2026
Merged

fix(progress) + feat(insights): eight defects, and a conclusion you can remove#602
mrviduus merged 5 commits into
mainfrom
fix/progress-path-debt

Conversation

@mrviduus

@mrviduus mrviduus commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Two things that arrived together: the progress-path debt recorded while tracing the assistant handoff, and what a three-way consilium on insight categories turned up while arguing.

Progress path — four defects fixed, two deliberately not

"Mark as finished" wrote two different things. Web sent the {"type":"end"} sentinel; mobile, from the same menu on the same kind of book, sent scroll:<lastSlug>:0 — so a book marked finished on the phone reopened at the top of its last chapter. One definition now (packages/shared/src/reader/progressLocators.ts), both clients import it.

The first write for a book skipped the rules. PUT /me/progress/{editionId} had two code paths writing one row: an update that checks ProgressUnit.IsTrusted, and an insert that assigned request.Percent by hand. So the first write for an edition kept an untrusted number every later write would have refused — and a book finished in a single write never got a CompletedAt. The insert now hands the row to the same ApplyProgressUpdate. ProgressUnitEndpointTests fails against the previous build and passes against this one.

ReadingProgressDto existed twice in TypeScript with nothing linking the copies.

A tap that did nothing now says so — "Talk this book over" swallowed openURL's rejection, which is right for "you have no browser" and indistinguishable from a broken button.

Not fixed, on purpose (written up in assistant-handoff.md so nobody "fixes" them): LocatorKind on the catalog path would refuse the very sentinel it was meant to protect — catalog editions have no second coordinate space; and markAsUnread not lowering MaxChapterNumber is moot, because that column's only reader was the RAG gate deleted with ask_book.

The web insights section threw on every signed-in book page

Found by the consilium, not by a test. The web-local insights client unwrapped .items — an envelope GET /me/insights does not send (it returns a bare list; the envelope belongs to /me/mcp/keys, where the line was copied from). The component stored undefined and read .length off it on the next render. Every signed-in BookDetailPage and UserBookDetailPage, with or without insights, in the change that was meant to make that section render at all. Its own test mocks the module wholesale, so it could not see the shape; the new test pins the wire shape instead.

A conclusion can be removed

Saving again replaces the insight for the same chapter, so a poor conclusion about the right chapter self-corrects. One filed against the wrong chapter never does — nothing revisits that slot. It was permanent.

DELETE /me/insights/{id}, reader-only, 404 (not 403) for someone else's id, hard delete. Deliberately not an MCP tool: save_insight's worst case is one bad paragraph removed in a tap; a delete tool's worst case is a year of конспект gone, from a stateless bridge that cannot confirm intent.

Also

Five web modules and one stylesheet with no importers deleted — three were listed as live in CLAUDE.md's own hook inventory.

Verification

  • dotnet format textstack.sln --verify-no-changes — clean
  • dotnet test tests/TextStack.UnitTests; integration --filter Insights (18) and --filter ProgressUnit (2) — pass against a locally rebuilt API
  • web 709 + 2 new delete-path tests, mobile 369, shared 449 — pass
  • tsc --noEmit on web and mobile — clean

🤖 Generated with Claude Code

https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E

mrviduus and others added 5 commits September 10, 2026 20:30
…ame rules

Four of the ten progress-path defects found while tracing the assistant handoff.

- "Mark as finished" wrote two different locators: web the {"type":"end"}
  sentinel, mobile scroll:<lastSlug>:0 — so a book finished on the phone reopened
  at the top of its last chapter. One definition now, in shared, imported by both.
- PUT /me/progress had two code paths writing one row. The insert assigned
  request.Percent by hand with no ProgressUnit check and never recorded
  completion, so the FIRST write for a book kept a number every later write would
  have refused, and a book finished in one write stayed unfinished. It now hands
  the row to the same ApplyProgressUpdate. Covered by ProgressUnitEndpointTests,
  which fails against the previous build.
- ReadingProgressDto existed twice in TypeScript with nothing linking the copies.
- "Talk this book over" swallowed openURL's rejection, leaving a tap that does
  nothing — the one outcome a reader cannot tell from a broken button.

Two defects are deliberately NOT fixed and the reasons are written down:
LocatorKind on the catalog path would refuse the mark-as-read sentinel it was
meant to protect, and MaxChapterNumber's only reader was the deleted RAG gate.

Also deletes five web modules and one stylesheet with no importers, and corrects
the hook inventory that listed three of them as live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
The web-local insights client unwrapped `.items`, an envelope the endpoint does
not send: GET /me/insights returns Results.Ok(dtos) over a bare List. The line
was copied from the mcpKeys client, where the envelope is real. The component
stored undefined and read .length off it on the next render, so every signed-in
BookDetailPage and UserBookDetailPage threw — with or without insights — in the
change that was meant to make the section render at all.

The component's own test mocks this module wholesale, so it could not see the
shape. The new test talks to the client and pins what the server actually sends,
including the empty case that is by far the most common (0 insights exist).

Found by an adversarial read of the insight lifecycle, not by a failing test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
A misfiled conclusion was permanent. Replacing covers the common miss — a poor
conclusion about the RIGHT chapter, which the assistant re-runs and overwrites —
but nothing ever revisits a slot the model did not mean to write, and no client
could remove one.

DELETE /me/insights/{id}, reader-only. 404 rather than 403 for someone else's id,
so the endpoint cannot be used to learn that an id exists. Hard delete, matching
the entity's stance (a конспект, not a log) and avoiding a deleted_at that would
have to enter both partial unique index filters — where the NULLS NOT DISTINCT
rule protecting the book-level row lives.

Deliberately NOT an MCP tool. save_insight's worst case is one bad paragraph the
reader removes in a tap; a delete tool's worst case is a year of конспект gone,
driven by a stateless bridge that cannot confirm intent against a table with no
soft-delete and no trash. The asymmetry is categorical, not a matter of degree.

Affordance on both clients, with the failure path tested: a delete the server
refuses leaves the row on screen, because it is still there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
… it did not

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
@mrviduus mrviduus changed the title fix(progress): one mark-as-finished, and an insert that follows the same rules fix(progress) + feat(insights): eight defects, and a conclusion you can remove Sep 11, 2026
@mrviduus
mrviduus merged commit 96488e6 into main Sep 11, 2026
10 checks passed
@mrviduus
mrviduus deleted the fix/progress-path-debt branch September 11, 2026 01:40
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