Skip to content

Record the leaf a run sent and never got back - #205

Merged
SearheiParkhamchuk merged 1 commit into
major/translatorfrom
fix/translator-report-returned-not-attempted
Oct 21, 2026
Merged

SearheiParkhamchuk merged 1 commit into
major/translatorfrom
fix/translator-report-returned-not-attempted

Conversation

@SearheiParkhamchuk

Copy link
Copy Markdown
Contributor

What was wrong

A run claimed every leaf it collected. If the provider answered three fields out of five, the
receipt recorded a source hash for all five, and the two it never answered read as freshly
translated for good.

Correcting that exposed a second, worse fault underneath it. A leaf sent and unanswered with no
earlier claim
— a first run, a legacy document-shaped receipt, a newly appeared address — was
recorded as DECLINED. That value is deliberately inert: matchesClaim returns true for it and
leafSourceChanged returns undefined, so the leaf never reads as out of date. Under
skip_existing it was never selected again, and it was not empty either, because the reconciler had
already seated the source text in its target. Nothing would ever pick it up.

What this changes

The pipeline reports an outcome per leaf it sent. PipelineResult.translatedPaths: IdPath[]
becomes leaves: readonly LeafOutcome[], one entry per leaf that yielded a chunk. The applicator
returns the chunks it actually wrote instead of a caller re-deriving it, so
LeafOutcome.answeredInFull is true only when every chunk a leaf was cut into came back and was
written. A leaf that produced no chunk is absent, which is not the same as unanswered.

A receipt records a third state. UNANSWERED reads as drifted, so the next run selects the leaf
again, and it clears the moment a run answers it. A leaf the run never sent keeps whatever the last
receipt claimed; a leaf the run sent and lost overwrites an earlier hash, because the target now
holds source text rather than that translation.

The rule lives in the schema and the types, because the column cannot hold it. The fingerprint
column is plain text. SourceHash is branded and minted in one place, the stored map is validated
against the three forms it may hold, and staleness.ts branches on a tagged Claim rather than on
a raw value. That combination is load-bearing, not decoration: with the brand removed, deleting the
UNANSWERED branch from claimOf compiles silently; with it, the compiler refuses.

Backward compatibility was checked rather than assumed. Per-leaf values have only ever been 16
lowercase hex or null — one commit over computeFieldFingerprints.ts, one writer. The legacy
64-hex digest lives in the document arm and is left unvalidated.

Not solved here

  • A run that answers nothing still writes a receipt with a fresh translatedAt, so the admin reads
    "translated just now" over a run that translated nothing.
  • The field-level endpoint writes no receipt at all.
  • Under skip_existing, a leaf the provider deterministically never answers is now re-sent every
    run. Repeated spend in place of silent permanence — the trade this change makes on purpose.

Checks

179 files / 2398 checks · check-types 0 · oxlint 44 warnings, 0 errors · turbo run build
green, which is the real tsc declaration emit rather than tsgo.

Four audits ran over the finished change — complexity, abstraction, a stub-and-mutation audit of the
suite, and comments. Two of them found checks that were green for the wrong reason: toEqual
ignores keys whose value is undefined, so a mutation removing a filter killed nothing; and one pin
did not test what its text claimed, because null kept the directive in use whether or not the
brand was there. Both are fixed, and the full findings are in
docs/plans/2026-10-21-the-leaf-that-never-came-back.task.md.

Closes #197

A run used to claim every leaf it collected, including the ones the provider
never answered. The pipeline now reports an outcome per leaf it sent —
`LeafOutcome { idPath, answered }` in place of `translatedPaths` — and the
applicator returns what it wrote, so a leaf counts as answered only when every
chunk it was cut into came back and was written.

That correction was half a cure. A leaf sent and unanswered with no earlier
claim was recorded as DECLINED, which is deliberately inert: it never reads as
out of date, so under skip_existing it was never selected again. The
reconciler has already seated the source text in its target, so the field is
not empty either, and nothing else would have picked it up. The leaf was lost.

A receipt now records a third state for that leaf. UNANSWERED reads as
drifted, so the next run selects it again, and it clears the moment a run
answers. The column is plain text and cannot hold that rule, so the parse
schema and the types carry it instead: SourceHash is branded, and the stored
map is validated against the three forms it may hold. Without the brand a
reader that drops the new state still compiles; with it, the compiler refuses.

Closes #197
@vercel

vercel Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ideal-cms Error Error Oct 21, 2026 7:23pm UTC

Request Review

@SearheiParkhamchuk SearheiParkhamchuk self-assigned this Oct 21, 2026
@SearheiParkhamchuk
SearheiParkhamchuk merged commit 40acd25 into major/translator Oct 21, 2026
1 of 2 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.0.0-next.3 🎉

The release is available on npm package (@next dist-tag)

Your semantic-release bot 📦🚀

This branch had an error being deployed

1 failed deployment
Preview — 69f1bda2 Deployed Oct 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant